Skip to documentation
Asteri/Developers

Developer Search

Search developer docs, public resources, and endpoint references.

Get started

  • Overview
  • Quickstart

Build

  • API basics
  • API reference
  • TypeScript SDK
  • Webhooks

Manage

  • Integration setup
  • Changelog

REST API v1

OpenAPI specificationContact Asteri

Build an integration

API reference

Find an endpoint, check its permissions, and copy a request example. All routes below use /api/public/v1.

API basicsOpen OpenAPI JSON

75 results across 27 resources

Browse resources
CustomersLeadsAppointmentsJob visitsEstimatesInvoicesPaymentsServicesFilesPhotosTagsChecklistsChecklist templatesPhoto reportsDocumentsTime entriesWebhook subscriptionsWebhook EventsConversationsMessagesDealsAvailabilityBookingsTasksCustomer notesWorkflowsTeam members

Customers

Customer identity, contact channels, service history, and organization-scoped records.

/api/public/v1/customers
GET
/api/public/v1/customersList customers in the active organization with filtering and pagination.

Required permissions

read:customers

Success:200

Request parameters

limitQuery · integer · Optional
Maximum records per page, from 1 to 100 (default 25).
offsetQuery · integer · Optional
Number of matching records to skip.
searchQuery · string · Optional
Filter by customer name or email.

Usage

cURL
curl -X GET \
  'http://localhost:3000/api/public/v1/customers' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.customers.list();

Response body

CustomerListResponse
{
  "data": [
    {
      "id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
      "organization_id": "dce3853d-4f94-4bc8-a816-4d0d74c11a9d",
      "account_type": "business",
      "display_name": "North Harbor Facilities",
      "email": "ops@northerborfacilities.com",
      "phone": "+1-404-555-0128",
      "created_at": "2026-03-18T15:40:12.000Z"
    }
  ],
  "pagination": {
    "count": 2,
    "limit": 25,
    "next_cursor": null
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 429 Rate limited · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Malformed query or request payload.

Usually happens when:
The request sends malformed query/body data.
What to do:
Validate request fields against the contract examples before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Missing plan access, capability, IP access, or scopes for this request.

Usually happens when:
The key is valid but the company’s plan does not include this (reads need a paid plan, writes need Premium or above), API access is suspended, the key IP allowlist does not match, or the key lacks the required scope.
What to do:
Upgrade the plan for writes, adjust the key IP allowlist, or create a key with the required scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle request volume and avoid immediate fan-out retries from parallel workers.
Retry guidance:
Honor the `Retry-After` header and use exponential backoff for repeated limits.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this request.
What to do:
Check API availability, then retry with backoff.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while reading or serializing the resource.
What to do:
Log the contract version and response body, then retry once before escalating to Asteri support.
Error response
{
  "error": "We couldn't load customers. Try again."
}
POST
/api/public/v1/customersCreate a customer with organization-scoped identity and contact data.

Required permissions

write:customers

Success:201

Usage

cURL
curl -X POST \
  'http://localhost:3000/api/public/v1/customers' \
  -H "Authorization: Bearer ast_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{
  "account_type": "business",
  "display_name": "North Harbor Facilities",
  "company_name": "North Harbor Facilities",
  "first_name": "Nora",
  "last_name": "Harbor",
  "email": "ops@northerborfacilities.com",
  "phone": "+1-404-555-0128"
}'
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.customers.create({
  "account_type": "business",
  "display_name": "North Harbor Facilities",
  "company_name": "North Harbor Facilities",
  "first_name": "Nora",
  "last_name": "Harbor",
  "email": "ops@northerborfacilities.com",
  "phone": "+1-404-555-0128"
});

Request body

Payload for creating a customer in the public API.

CreateCustomerRequest
{
  "account_type": "business",
  "display_name": "North Harbor Facilities",
  "company_name": "North Harbor Facilities",
  "first_name": "Nora",
  "last_name": "Harbor",
  "email": "ops@northerborfacilities.com",
  "phone": "+1-404-555-0128"
}
View request fields
CreateCustomerRequest fields
FieldTypeRequiredDescription
account_type"individual" | "business"YesAccount classification used when creating the customer.
display_namestringNoCustomer display name or company name. Useful when you do not want to provide explicit name fields.
company_namestringNoExplicit company name for business accounts.
first_namestringNoPrimary contact first name.
last_namestringNoPrimary contact last name.
emailstringYesPrimary contact email.
phonestringYesPrimary contact phone number.

Response body

CreateCustomerResponse
{
  "data": {
    "id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
    "organization_id": "dce3853d-4f94-4bc8-a816-4d0d74c11a9d",
    "account_type": "business",
    "display_name": "North Harbor Facilities",
    "email": "ops@northerborfacilities.com",
    "phone": "+1-404-555-0128",
    "created_at": "2026-03-18T15:40:12.000Z"
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 409 Conflict · 429 Rate limited · 413 Request too large · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Invalid request payload.

Usually happens when:
The request body is missing required fields or includes invalid field values.
What to do:
Validate the payload against the generated request schema and fix any `issues` entries before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Creating and changing records needs the Premium plan or above.

Usually happens when:
The company plan includes reading through the API but not writing, or the key lacks the write scope.
What to do:
Upgrade to Premium in Subscription settings, or create a key with the needed write scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

409 Conflict

Duplicate customer email or conflicting customer state.

Usually happens when:
A create request reuses a primary email that already belongs to another active customer in the same organization.
What to do:
Fetch the existing customer first or update your upstream dedupe logic before retrying the create.
Error response
{
  "error": "A customer already uses this email. Use a different email or open the existing customer.",
  "field": "email"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle mutation throughput and avoid blind retries from queues or worker pools.
Retry guidance:
Honor the `Retry-After` header and replay writes only after the relevant limit window resets.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

413 Request too large

The JSON request body exceeds this route’s byte limit.

Usually happens when:
The request body is larger than this write route accepts.
What to do:
Reduce the request body and try again.
Error response
{
  "error": "The request is too large. Send a smaller JSON body and try again."
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this write.
What to do:
Check API availability, then retry with the same idempotency key when applicable.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while validating or persisting the mutation.
What to do:
Capture the response payload and contract version, then retry once before escalating.
Error response
{
  "error": "We couldn't load customers. Try again."
}
PATCH
/api/public/v1/customers/{customerId}Change a customer’s name, company, email or phone. Send only the fields that change.

Required permissions

write:customers

Success:200

Usage

cURL
curl -X PATCH \
  'http://localhost:3000/api/public/v1/customers/8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3' \
  -H "Authorization: Bearer ast_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{
  "first_name": "Nora",
  "last_name": "Harper",
  "company_name": "North Harbor Facilities",
  "email": "nora@example.com",
  "phone": "+15555550100"
}'
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.customers.update({ customerId: '8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3' }, {
  "first_name": "Nora",
  "last_name": "Harper",
  "company_name": "North Harbor Facilities",
  "email": "nora@example.com",
  "phone": "+15555550100"
});

Request body

Payload for changing a customer. Send only the fields that change.

UpdateCustomerRequest
{
  "first_name": "Nora",
  "last_name": "Harper",
  "company_name": "North Harbor Facilities",
  "email": "nora@example.com",
  "phone": "+15555550100"
}
View request fields
UpdateCustomerRequest fields
FieldTypeRequiredDescription
first_namestringNoPrimary contact first name.
last_namestringNoPrimary contact last name.
company_namestring or nullNoCompany name for business accounts. Send null to clear it.
emailstring or nullNoPrimary email address. Send null to remove it.
phonestring or nullNoPrimary phone number. Send null to remove it.

Response body

UpdateCustomerResponse
{
  "data": {
    "id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
    "organization_id": "dce3853d-4f94-4bc8-a816-4d0d74c11a9d",
    "account_type": "business",
    "display_name": "North Harbor Facilities",
    "email": "ops@northerborfacilities.com",
    "phone": "+1-404-555-0128",
    "created_at": "2026-03-18T15:40:12.000Z"
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 409 Conflict · 429 Rate limited · 413 Request too large · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Invalid request payload.

Usually happens when:
The request body is missing required fields or includes invalid field values.
What to do:
Validate the payload against the generated request schema and fix any `issues` entries before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Creating and changing records needs the Premium plan or above.

Usually happens when:
The company plan includes reading through the API but not writing, or the key lacks the write scope.
What to do:
Upgrade to Premium in Subscription settings, or create a key with the needed write scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

409 Conflict

Another customer in this company already uses the new email address.

Usually happens when:
The email sent belongs to another active customer in the same organization.
What to do:
Find that customer and update it instead, or send a different email address.
Error response
{
  "error": "A customer already uses this email. Use a different email or open the existing customer.",
  "field": "email"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle mutation throughput and avoid blind retries from queues or worker pools.
Retry guidance:
Honor the `Retry-After` header and replay writes only after the relevant limit window resets.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

413 Request too large

The JSON request body exceeds this route’s byte limit.

Usually happens when:
The request body is larger than this write route accepts.
What to do:
Reduce the request body and try again.
Error response
{
  "error": "The request is too large. Send a smaller JSON body and try again."
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this write.
What to do:
Check API availability, then retry with the same idempotency key when applicable.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while validating or persisting the mutation.
What to do:
Capture the response payload and contract version, then retry once before escalating.
Error response
{
  "error": "We couldn't load customers. Try again."
}
GET
/api/public/v1/customers/{customerId}Retrieve one customer and related profile metadata.

Required permissions

read:customers

Success:200

Usage

cURL
curl -X GET \
  'http://localhost:3000/api/public/v1/customers/8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.customers.get({ customerId: '8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3' });

Response body

CustomerResponse
{
  "data": {
    "id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
    "organization_id": "dce3853d-4f94-4bc8-a816-4d0d74c11a9d",
    "account_type": "business",
    "display_name": "North Harbor Facilities",
    "email": "ops@northerborfacilities.com",
    "phone": "+1-404-555-0128",
    "created_at": "2026-03-18T15:40:12.000Z"
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 404 Not found · 429 Rate limited · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Malformed query or request payload.

Usually happens when:
The request sends malformed query/body data.
What to do:
Validate request fields against the contract examples before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Missing plan access, capability, IP access, or scopes for this request.

Usually happens when:
The key is valid but the company’s plan does not include this (reads need a paid plan, writes need Premium or above), API access is suspended, the key IP allowlist does not match, or the key lacks the required scope.
What to do:
Upgrade the plan for writes, adjust the key IP allowlist, or create a key with the required scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

404 Not found

Requested customer record was not found.

Usually happens when:
The requested `customerId` does not exist in the active organization or has been removed from the public view.
What to do:
Refresh your local record cache and confirm the customer belongs to the organization id you sent.
Error response
{
  "error": "We couldn't find that customer. Check its ID and try again."
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle request volume and avoid immediate fan-out retries from parallel workers.
Retry guidance:
Honor the `Retry-After` header and use exponential backoff for repeated limits.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this request.
What to do:
Check API availability, then retry with backoff.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while reading or serializing the resource.
What to do:
Log the contract version and response body, then retry once before escalating to Asteri support.
Error response
{
  "error": "We couldn't load customers. Try again."
}
View customers schemas and field examples

Customer

Canonical public representation of a customer record in Asteri.

Customer fields
FieldTypeRequiredDescription
iduuidYesStable customer identifier.
organization_iduuidYesOrganization that owns the record.
account_type"individual" | "business"YesHigh-level account classification.
display_namestringYesPrimary display name shown across the platform.
emailstring | nullYesPrimary contact email.
phonestring | nullYesPrimary contact phone number.
created_atdatetimeYesISO-8601 timestamp for record creation.
Example Customer
{
  "id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
  "organization_id": "dce3853d-4f94-4bc8-a816-4d0d74c11a9d",
  "account_type": "business",
  "display_name": "North Harbor Facilities",
  "email": "ops@northerborfacilities.com",
  "phone": "+1-404-555-0128",
  "created_at": "2026-03-18T15:40:12.000Z"
}

CreateCustomerRequest

Payload for creating a customer in the public API.

CreateCustomerRequest fields
FieldTypeRequiredDescription
account_type"individual" | "business"YesAccount classification used when creating the customer.
display_namestringNoCustomer display name or company name. Useful when you do not want to provide explicit name fields.
company_namestringNoExplicit company name for business accounts.
first_namestringNoPrimary contact first name.
last_namestringNoPrimary contact last name.
emailstringYesPrimary contact email.
phonestringYesPrimary contact phone number.
Example CreateCustomerRequest
{
  "account_type": "business",
  "display_name": "North Harbor Facilities",
  "company_name": "North Harbor Facilities",
  "first_name": "Nora",
  "last_name": "Harbor",
  "email": "ops@northerborfacilities.com",
  "phone": "+1-404-555-0128"
}

UpdateCustomerRequest

Payload for changing a customer. Send only the fields that change.

UpdateCustomerRequest fields
FieldTypeRequiredDescription
first_namestringNoPrimary contact first name.
last_namestringNoPrimary contact last name.
company_namestring or nullNoCompany name for business accounts. Send null to clear it.
emailstring or nullNoPrimary email address. Send null to remove it.
phonestring or nullNoPrimary phone number. Send null to remove it.
Example UpdateCustomerRequest
{
  "first_name": "Nora",
  "last_name": "Harper",
  "company_name": "North Harbor Facilities",
  "email": "nora@example.com",
  "phone": "+15555550100"
}

CustomerListResponse

Response body for customer list endpoints.

CustomerListResponse fields
FieldTypeRequiredDescription
dataCustomer[]YesCollection of customers matching the current filter set.
paginationPaginationYesList metadata for page traversal.
Example CustomerListResponse
{
  "data": [
    {
      "id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
      "organization_id": "dce3853d-4f94-4bc8-a816-4d0d74c11a9d",
      "account_type": "business",
      "display_name": "North Harbor Facilities",
      "email": "ops@northerborfacilities.com",
      "phone": "+1-404-555-0128",
      "created_at": "2026-03-18T15:40:12.000Z"
    }
  ],
  "pagination": {
    "count": 2,
    "limit": 25,
    "next_cursor": null
  }
}

Leads

Inbound lead capture for website forms and lightweight source integrations.

/api/public/v1/leads
POST
/api/public/v1/leadsCapture an inbound lead without creating a customer.

Required permissions

write:leads

Success:201

Usage

cURL
curl -X POST \
  'http://localhost:3000/api/public/v1/leads' \
  -H "Authorization: Bearer ast_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{
  "source": "website",
  "external_id": "form-submission-1001",
  "name": "Nora Harbor",
  "first_name": "Nora",
  "last_name": "Harbor",
  "email": "nora@example.com",
  "phone": "(443) 252-0607",
  "company": "North Harbor Facilities",
  "service_type": "carpet_cleaning",
  "message": "Can you call me about more information on this service?",
  "priority": "medium",
  "value_estimate": 450,
  "landing_page": "https://ecodry.example/contact",
  "origin_host": "ecodry.example",
  "referrer": "https://google.com/",
  "utm_source": "google",
  "utm_medium": "cpc",
  "utm_campaign": "spring-cleaning",
  "utm_term": "carpet cleaner",
  "utm_content": "contact-form",
  "utm_id": "spring-2026-id",
  "utm_source_platform": "google_ads",
  "utm_creative_format": "video",
  "gclid": "test-gclid",
  "gbraid": "test-gbraid",
  "wbraid": "test-wbraid",
  "gad_source": "1",
  "gad_campaignid": "123456789",
  "gad_campaign_id": "123456789",
  "gclsrc": "aw.ds",
  "dclid": "test-dclid",
  "srsltid": "test-srsltid",
  "fbclid": "test-fbclid",
  "msclkid": "test-msclkid",
  "ttclid": "test-ttclid",
  "twclid": "test-twclid",
  "sc_cid": "test-sccid",
  "rdt_cid": "test-rdt-cid",
  "epik": "test-epik",
  "li_fat_id": "test-li-fat-id",
  "mc_cid": "test-mc-cid",
  "mc_eid": "test-mc-eid",
  "campaign_id": "campaign-123",
  "adgroup_id": "adgroup-123",
  "ad_group_id": "adgroup-123",
  "ad_id": "ad-123",
  "adset_id": "adset-123",
  "ad_set_id": "adset-123",
  "placement": "feed",
  "match_type": "phrase",
  "device": "m",
  "network": "g",
  "keyword": "carpet cleaner",
  "matchtype": "phrase",
  "address_line1": "1200 Harbor Way",
  "address_line2": "Suite 4",
  "city": "Forest Hill",
  "region": "MD",
  "postal_code": "21050",
  "country": "US",
  "captured_at": "2026-03-23T16:10:00.000Z"
}'
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.leads.create({
  "source": "website",
  "external_id": "form-submission-1001",
  "name": "Nora Harbor",
  "first_name": "Nora",
  "last_name": "Harbor",
  "email": "nora@example.com",
  "phone": "(443) 252-0607",
  "company": "North Harbor Facilities",
  "service_type": "carpet_cleaning",
  "message": "Can you call me about more information on this service?",
  "priority": "medium",
  "value_estimate": 450,
  "landing_page": "https://ecodry.example/contact",
  "origin_host": "ecodry.example",
  "referrer": "https://google.com/",
  "utm_source": "google",
  "utm_medium": "cpc",
  "utm_campaign": "spring-cleaning",
  "utm_term": "carpet cleaner",
  "utm_content": "contact-form",
  "utm_id": "spring-2026-id",
  "utm_source_platform": "google_ads",
  "utm_creative_format": "video",
  "gclid": "test-gclid",
  "gbraid": "test-gbraid",
  "wbraid": "test-wbraid",
  "gad_source": "1",
  "gad_campaignid": "123456789",
  "gad_campaign_id": "123456789",
  "gclsrc": "aw.ds",
  "dclid": "test-dclid",
  "srsltid": "test-srsltid",
  "fbclid": "test-fbclid",
  "msclkid": "test-msclkid",
  "ttclid": "test-ttclid",
  "twclid": "test-twclid",
  "sc_cid": "test-sccid",
  "rdt_cid": "test-rdt-cid",
  "epik": "test-epik",
  "li_fat_id": "test-li-fat-id",
  "mc_cid": "test-mc-cid",
  "mc_eid": "test-mc-eid",
  "campaign_id": "campaign-123",
  "adgroup_id": "adgroup-123",
  "ad_group_id": "adgroup-123",
  "ad_id": "ad-123",
  "adset_id": "adset-123",
  "ad_set_id": "adset-123",
  "placement": "feed",
  "match_type": "phrase",
  "device": "m",
  "network": "g",
  "keyword": "carpet cleaner",
  "matchtype": "phrase",
  "address_line1": "1200 Harbor Way",
  "address_line2": "Suite 4",
  "city": "Forest Hill",
  "region": "MD",
  "postal_code": "21050",
  "country": "US",
  "captured_at": "2026-03-23T16:10:00.000Z"
});

Request body

Payload for capturing an inbound lead. This creates a Leads Hub row, not a customer.

CreateLeadRequest
{
  "source": "website",
  "external_id": "form-submission-1001",
  "name": "Nora Harbor",
  "first_name": "Nora",
  "last_name": "Harbor",
  "email": "nora@example.com",
  "phone": "(443) 252-0607",
  "company": "North Harbor Facilities",
  "service_type": "carpet_cleaning",
  "message": "Can you call me about more information on this service?",
  "priority": "medium",
  "value_estimate": 450,
  "landing_page": "https://ecodry.example/contact",
  "origin_host": "ecodry.example",
  "referrer": "https://google.com/",
  "utm_source": "google",
  "utm_medium": "cpc",
  "utm_campaign": "spring-cleaning",
  "utm_term": "carpet cleaner",
  "utm_content": "contact-form",
  "utm_id": "spring-2026-id",
  "utm_source_platform": "google_ads",
  "utm_creative_format": "video",
  "gclid": "test-gclid",
  "gbraid": "test-gbraid",
  "wbraid": "test-wbraid",
  "gad_source": "1",
  "gad_campaignid": "123456789",
  "gad_campaign_id": "123456789",
  "gclsrc": "aw.ds",
  "dclid": "test-dclid",
  "srsltid": "test-srsltid",
  "fbclid": "test-fbclid",
  "msclkid": "test-msclkid",
  "ttclid": "test-ttclid",
  "twclid": "test-twclid",
  "sc_cid": "test-sccid",
  "rdt_cid": "test-rdt-cid",
  "epik": "test-epik",
  "li_fat_id": "test-li-fat-id",
  "mc_cid": "test-mc-cid",
  "mc_eid": "test-mc-eid",
  "campaign_id": "campaign-123",
  "adgroup_id": "adgroup-123",
  "ad_group_id": "adgroup-123",
  "ad_id": "ad-123",
  "adset_id": "adset-123",
  "ad_set_id": "adset-123",
  "placement": "feed",
  "match_type": "phrase",
  "device": "m",
  "network": "g",
  "keyword": "carpet cleaner",
  "matchtype": "phrase",
  "address_line1": "1200 Harbor Way",
  "address_line2": "Suite 4",
  "city": "Forest Hill",
  "region": "MD",
  "postal_code": "21050",
  "country": "US",
  "captured_at": "2026-03-23T16:10:00.000Z"
}
View request fields
CreateLeadRequest fields
FieldTypeRequiredDescription
sourceLeadSourceNoInbound lead source. Defaults to `website` for lead form submissions.
external_idstringNoOptional upstream form submission id for dedupe.
namestringNoLead contact full name.
first_namestringNoLead contact first name.
last_namestringNoLead contact last name.
emailstringNoLead contact email.
phonestringNoLead contact phone number.
companystringNoCompany name for commercial inquiries.
service_typestringNoRequested service category.
messagestringNoFree-form lead note or project details.
priorityLeadPriorityNoOptional inbound priority hint.
value_estimatenumberNoOptional estimated lead value.
landing_pagestringNoLanding page where the form was submitted.
origin_hoststringNoSubmitting website host, if already known.
referrerstringNoHTTP referrer or upstream source URL.
utm_sourcestringNoUTM source attribution value.
utm_mediumstringNoUTM medium attribution value.
utm_campaignstringNoUTM campaign attribution value.
utm_termstringNoUTM term attribution value.
utm_contentstringNoUTM content attribution value.
utm_idstringNoOptional UTM campaign id.
utm_source_platformstringNoOptional source platform attribution value.
utm_creative_formatstringNoOptional creative format attribution value.
gclidstringNoGoogle Ads click id.
gbraidstringNoGoogle Ads iOS app-to-web click id.
wbraidstringNoGoogle Ads web-to-app click id.
gad_sourcestringNoGoogle Ads aggregate ad source parameter.
gad_campaignidstringNoGoogle Ads aggregate campaign id parameter.
gad_campaign_idstringNoAlias for Google Ads aggregate campaign id parameter.
gclsrcstringNoGoogle click source parameter.
dclidstringNoGoogle Display click id.
srsltidstringNoGoogle Merchant Center auto-tagging id.
fbclidstringNoMeta click id.
msclkidstringNoMicrosoft Ads click id.
ttclidstringNoTikTok click id.
twclidstringNoX Ads click id.
sc_cidstringNoSnapchat click id.
rdt_cidstringNoReddit Ads click id.
epikstringNoPinterest click id.
li_fat_idstringNoLinkedIn click id.
mc_cidstringNoMailchimp campaign id.
mc_eidstringNoMailchimp email recipient id.
campaign_idstringNoUpstream campaign id.
adgroup_idstringNoGoogle Ads ad group id from the final URL suffix.
ad_group_idstringNoAlias for upstream ad group id.
ad_idstringNoUpstream ad or creative id.
adset_idstringNoUpstream ad set id.
ad_set_idstringNoAlias for upstream ad set id.
placementstringNoUpstream placement value.
match_typestringNoAlias for paid search match type.
devicestringNoGoogle Ads device segment from the final URL suffix.
networkstringNoGoogle Ads network segment from the final URL suffix.
keywordstringNoPaid search keyword or query term.
matchtypestringNoPaid search match type.
address_line1stringNoService address line 1.
address_line2stringNoService address line 2.
citystringNoService address city.
regionstringNoService address region or state.
postal_codestringNoService address postal code.
countrystringNoService address country.
captured_atdatetimeNoOptional capture timestamp from the submitting form.

Response body

CreateLeadResponse
{
  "data": {
    "id": "5db69b49-73b2-4bb1-9694-508481a9dbac",
    "organization_id": "dce3853d-4f94-4bc8-a816-4d0d74c11a9d",
    "source": "website",
    "status": "new",
    "customer_id": null,
    "display_name": "Nora Harbor",
    "email": "nora@example.com",
    "phone": "+14432520607",
    "service_type": "carpet_cleaning",
    "message": "Can you call me about more information on this service?",
    "landing_page": "https://ecodry.example/contact",
    "origin_host": "ecodry.example",
    "captured_at": "2026-03-23T16:10:00.000Z",
    "created_at": "2026-03-23T16:10:00.000Z"
  },
  "was_created": true,
  "matched_customer_id": null
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 429 Rate limited · 413 Request too large · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Invalid request payload.

Usually happens when:
The request body is missing required fields or includes invalid field values.
What to do:
Validate the payload against the generated request schema and fix any `issues` entries before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Creating and changing records needs the Premium plan or above.

Usually happens when:
The company plan includes reading through the API but not writing, or the key lacks the write scope.
What to do:
Upgrade to Premium in Subscription settings, or create a key with the needed write scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle mutation throughput and avoid blind retries from queues or worker pools.
Retry guidance:
Honor the `Retry-After` header and replay writes only after the relevant limit window resets.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

413 Request too large

The JSON request body exceeds this route’s byte limit.

Usually happens when:
The request body is larger than this write route accepts.
What to do:
Reduce the request body and try again.
Error response
{
  "error": "The request is too large. Send a smaller JSON body and try again."
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this write.
What to do:
Check API availability, then retry with the same idempotency key when applicable.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while validating or persisting the mutation.
What to do:
Capture the response payload and contract version, then retry once before escalating.
Error response
{
  "error": "We couldn't load customers. Try again."
}
View leads schemas and field examples

Lead

Public representation of an inbound lead captured into the Leads Hub.

Lead fields
FieldTypeRequiredDescription
iduuidYesStable lead identifier.
organization_iduuidYesOrganization that owns the lead.
sourceLeadSourceYesInbound channel that produced the lead.
statusLeadStatusYesCurrent Leads Hub status.
customer_iduuid | nullYesExisting matched customer, when one was found. Lead capture does not create a customer.
display_namestring | nullYesLead contact display name.
emailstring | nullYesNormalized contact email, when provided.
phonestring | nullYesNormalized contact phone, when provided.
service_typestring | nullYesRequested service category.
messagestring | nullYesLead message preview.
landing_pagestring | nullYesLanding page where the lead form was submitted.
origin_hoststring | nullYesOrigin host for attribution and source diagnostics.
captured_atdatetimeYesTimestamp when the lead was captured.
created_atdatetimeYesTimestamp when the lead row was created.
Example Lead
{
  "id": "5db69b49-73b2-4bb1-9694-508481a9dbac",
  "organization_id": "dce3853d-4f94-4bc8-a816-4d0d74c11a9d",
  "source": "website",
  "status": "new",
  "customer_id": null,
  "display_name": "Nora Harbor",
  "email": "nora@example.com",
  "phone": "+14432520607",
  "service_type": "carpet_cleaning",
  "message": "Can you call me about more information on this service?",
  "landing_page": "https://ecodry.example/contact",
  "origin_host": "ecodry.example",
  "captured_at": "2026-03-23T16:10:00.000Z",
  "created_at": "2026-03-23T16:10:00.000Z"
}

CreateLeadRequest

Payload for capturing an inbound lead. This creates a Leads Hub row, not a customer.

CreateLeadRequest fields
FieldTypeRequiredDescription
sourceLeadSourceNoInbound lead source. Defaults to `website` for lead form submissions.
external_idstringNoOptional upstream form submission id for dedupe.
namestringNoLead contact full name.
first_namestringNoLead contact first name.
last_namestringNoLead contact last name.
emailstringNoLead contact email.
phonestringNoLead contact phone number.
companystringNoCompany name for commercial inquiries.
service_typestringNoRequested service category.
messagestringNoFree-form lead note or project details.
priorityLeadPriorityNoOptional inbound priority hint.
value_estimatenumberNoOptional estimated lead value.
landing_pagestringNoLanding page where the form was submitted.
origin_hoststringNoSubmitting website host, if already known.
referrerstringNoHTTP referrer or upstream source URL.
utm_sourcestringNoUTM source attribution value.
utm_mediumstringNoUTM medium attribution value.
utm_campaignstringNoUTM campaign attribution value.
utm_termstringNoUTM term attribution value.
utm_contentstringNoUTM content attribution value.
utm_idstringNoOptional UTM campaign id.
utm_source_platformstringNoOptional source platform attribution value.
utm_creative_formatstringNoOptional creative format attribution value.
gclidstringNoGoogle Ads click id.
gbraidstringNoGoogle Ads iOS app-to-web click id.
wbraidstringNoGoogle Ads web-to-app click id.
gad_sourcestringNoGoogle Ads aggregate ad source parameter.
gad_campaignidstringNoGoogle Ads aggregate campaign id parameter.
gad_campaign_idstringNoAlias for Google Ads aggregate campaign id parameter.
gclsrcstringNoGoogle click source parameter.
dclidstringNoGoogle Display click id.
srsltidstringNoGoogle Merchant Center auto-tagging id.
fbclidstringNoMeta click id.
msclkidstringNoMicrosoft Ads click id.
ttclidstringNoTikTok click id.
twclidstringNoX Ads click id.
sc_cidstringNoSnapchat click id.
rdt_cidstringNoReddit Ads click id.
epikstringNoPinterest click id.
li_fat_idstringNoLinkedIn click id.
mc_cidstringNoMailchimp campaign id.
mc_eidstringNoMailchimp email recipient id.
campaign_idstringNoUpstream campaign id.
adgroup_idstringNoGoogle Ads ad group id from the final URL suffix.
ad_group_idstringNoAlias for upstream ad group id.
ad_idstringNoUpstream ad or creative id.
adset_idstringNoUpstream ad set id.
ad_set_idstringNoAlias for upstream ad set id.
placementstringNoUpstream placement value.
match_typestringNoAlias for paid search match type.
devicestringNoGoogle Ads device segment from the final URL suffix.
networkstringNoGoogle Ads network segment from the final URL suffix.
keywordstringNoPaid search keyword or query term.
matchtypestringNoPaid search match type.
address_line1stringNoService address line 1.
address_line2stringNoService address line 2.
citystringNoService address city.
regionstringNoService address region or state.
postal_codestringNoService address postal code.
countrystringNoService address country.
captured_atdatetimeNoOptional capture timestamp from the submitting form.
Example CreateLeadRequest
{
  "source": "website",
  "external_id": "form-submission-1001",
  "name": "Nora Harbor",
  "first_name": "Nora",
  "last_name": "Harbor",
  "email": "nora@example.com",
  "phone": "(443) 252-0607",
  "company": "North Harbor Facilities",
  "service_type": "carpet_cleaning",
  "message": "Can you call me about more information on this service?",
  "priority": "medium",
  "value_estimate": 450,
  "landing_page": "https://ecodry.example/contact",
  "origin_host": "ecodry.example",
  "referrer": "https://google.com/",
  "utm_source": "google",
  "utm_medium": "cpc",
  "utm_campaign": "spring-cleaning",
  "utm_term": "carpet cleaner",
  "utm_content": "contact-form",
  "utm_id": "spring-2026-id",
  "utm_source_platform": "google_ads",
  "utm_creative_format": "video",
  "gclid": "test-gclid",
  "gbraid": "test-gbraid",
  "wbraid": "test-wbraid",
  "gad_source": "1",
  "gad_campaignid": "123456789",
  "gad_campaign_id": "123456789",
  "gclsrc": "aw.ds",
  "dclid": "test-dclid",
  "srsltid": "test-srsltid",
  "fbclid": "test-fbclid",
  "msclkid": "test-msclkid",
  "ttclid": "test-ttclid",
  "twclid": "test-twclid",
  "sc_cid": "test-sccid",
  "rdt_cid": "test-rdt-cid",
  "epik": "test-epik",
  "li_fat_id": "test-li-fat-id",
  "mc_cid": "test-mc-cid",
  "mc_eid": "test-mc-eid",
  "campaign_id": "campaign-123",
  "adgroup_id": "adgroup-123",
  "ad_group_id": "adgroup-123",
  "ad_id": "ad-123",
  "adset_id": "adset-123",
  "ad_set_id": "adset-123",
  "placement": "feed",
  "match_type": "phrase",
  "device": "m",
  "network": "g",
  "keyword": "carpet cleaner",
  "matchtype": "phrase",
  "address_line1": "1200 Harbor Way",
  "address_line2": "Suite 4",
  "city": "Forest Hill",
  "region": "MD",
  "postal_code": "21050",
  "country": "US",
  "captured_at": "2026-03-23T16:10:00.000Z"
}

CreateLeadResponse

Response returned after public API lead capture.

CreateLeadResponse fields
FieldTypeRequiredDescription
dataLeadYesCaptured lead object.
was_createdbooleanYesWhether a new lead row was created instead of deduping to an existing open lead.
matched_customer_iduuid | nullYesExisting matched customer id, if lead contact data matched one. No customer is created by lead capture.
Example CreateLeadResponse
{
  "data": {
    "id": "5db69b49-73b2-4bb1-9694-508481a9dbac",
    "organization_id": "dce3853d-4f94-4bc8-a816-4d0d74c11a9d",
    "source": "website",
    "status": "new",
    "customer_id": null,
    "display_name": "Nora Harbor",
    "email": "nora@example.com",
    "phone": "+14432520607",
    "service_type": "carpet_cleaning",
    "message": "Can you call me about more information on this service?",
    "landing_page": "https://ecodry.example/contact",
    "origin_host": "ecodry.example",
    "captured_at": "2026-03-23T16:10:00.000Z",
    "created_at": "2026-03-23T16:10:00.000Z"
  },
  "was_created": true,
  "matched_customer_id": null
}

Appointments

Scheduling lifecycle, assignment state, completion events, and operational context.

/api/public/v1/appointments
GET
/api/public/v1/appointmentsList appointments by date range, status, customer, or technician.

Required permissions

read:appointments

Success:200

Request parameters

limitQuery · integer · Optional
Maximum records per page, from 1 to 100 (default 25).
offsetQuery · integer · Optional
Number of matching records to skip.
statusQuery · string · Optional
Filter by appointment status.
customerIdQuery · uuid · Optional
Filter by customer ID.
teamMemberIdQuery · uuid · Optional
Filter by assigned team member ID.
startDateQuery · datetime · Optional
Earliest scheduled start.
endDateQuery · datetime · Optional
Latest scheduled start.

Usage

cURL
curl -X GET \
  'http://localhost:3000/api/public/v1/appointments' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.appointments.list();

Response body

AppointmentListResponse
{
  "data": [
    {
      "id": "10bdff89-2f4f-4f06-8595-5f971ca0119a",
      "customer_id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
      "status": "confirmed",
      "scheduled_start": "2026-03-24T13:00:00.000Z",
      "scheduled_end": "2026-03-24T15:00:00.000Z",
      "assigned_team_member_ids": [
        "95cb6a1e-3792-451a-b905-011946edc191"
      ]
    }
  ],
  "pagination": {
    "count": 2,
    "limit": 25,
    "next_cursor": null
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 429 Rate limited · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Malformed query or request payload.

Usually happens when:
The request sends malformed query/body data.
What to do:
Validate request fields against the contract examples before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Missing plan access, capability, IP access, or scopes for this request.

Usually happens when:
The key is valid but the company’s plan does not include this (reads need a paid plan, writes need Premium or above), API access is suspended, the key IP allowlist does not match, or the key lacks the required scope.
What to do:
Upgrade the plan for writes, adjust the key IP allowlist, or create a key with the required scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle request volume and avoid immediate fan-out retries from parallel workers.
Retry guidance:
Honor the `Retry-After` header and use exponential backoff for repeated limits.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this request.
What to do:
Check API availability, then retry with backoff.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while reading or serializing the resource.
What to do:
Log the contract version and response body, then retry once before escalating to Asteri support.
Error response
{
  "error": "We couldn't load customers. Try again."
}
POST
/api/public/v1/appointmentsCreate a scheduled appointment with service, technician, and timing context.

Required permissions

write:appointments

Success:201

Request parameters

Idempotency-KeyHeader · string · Required
A unique key for safe retries of this create request.

Usage

cURL
curl -X POST \
  'http://localhost:3000/api/public/v1/appointments' \
  -H "Authorization: Bearer ast_live_your_key" \
  -H "Idempotency-Key: partner-request-001" \
  -H "Content-Type: application/json" \
  -d '{
  "customer_id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
  "scheduled_start": "2026-03-24T13:00:00.000Z",
  "scheduled_end": "2026-03-24T15:00:00.000Z",
  "assigned_team_member_ids": [
    "95cb6a1e-3792-451a-b905-011946edc191"
  ]
}'
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.appointments.create({
  "customer_id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
  "scheduled_start": "2026-03-24T13:00:00.000Z",
  "scheduled_end": "2026-03-24T15:00:00.000Z",
  "assigned_team_member_ids": [
    "95cb6a1e-3792-451a-b905-011946edc191"
  ]
}, { idempotencyKey: 'partner-request-001' });

Request body

Payload for creating or updating appointments.

AppointmentUpsertRequest
{
  "customer_id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
  "scheduled_start": "2026-03-24T13:00:00.000Z",
  "scheduled_end": "2026-03-24T15:00:00.000Z",
  "assigned_team_member_ids": [
    "95cb6a1e-3792-451a-b905-011946edc191"
  ]
}
View request fields
AppointmentUpsertRequest fields
FieldTypeRequiredDescription
customer_iduuidYesCustomer that owns the scheduled work.
scheduled_startdatetimeYesAppointment start timestamp.
scheduled_enddatetimeYesAppointment end timestamp.
assigned_team_member_idsuuid[]NoAssigned technicians or team members.

Response body

CreateAppointmentResponse
{
  "data": {
    "id": "10bdff89-2f4f-4f06-8595-5f971ca0119a",
    "customer_id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
    "status": "confirmed",
    "scheduled_start": "2026-03-24T13:00:00.000Z",
    "scheduled_end": "2026-03-24T15:00:00.000Z",
    "assigned_team_member_ids": [
      "95cb6a1e-3792-451a-b905-011946edc191"
    ]
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 429 Rate limited · 413 Request too large · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Invalid request payload.

Usually happens when:
The request body is missing required fields or includes invalid field values.
What to do:
Validate the payload against the generated request schema and fix any `issues` entries before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Creating and changing records needs the Premium plan or above.

Usually happens when:
The company plan includes reading through the API but not writing, or the key lacks the write scope.
What to do:
Upgrade to Premium in Subscription settings, or create a key with the needed write scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle mutation throughput and avoid blind retries from queues or worker pools.
Retry guidance:
Honor the `Retry-After` header and replay writes only after the relevant limit window resets.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

413 Request too large

The JSON request body exceeds this route’s byte limit.

Usually happens when:
The request body is larger than this write route accepts.
What to do:
Reduce the request body and try again.
Error response
{
  "error": "The request is too large. Send a smaller JSON body and try again."
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this write.
What to do:
Check API availability, then retry with the same idempotency key when applicable.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while validating or persisting the mutation.
What to do:
Capture the response payload and contract version, then retry once before escalating.
Error response
{
  "error": "We couldn't load customers. Try again."
}
GET
/api/public/v1/appointments/{appointmentId}Retrieve one appointment and assignment state.

Required permissions

read:appointments

Success:200

Usage

cURL
curl -X GET \
  'http://localhost:3000/api/public/v1/appointments/10bdff89-2f4f-4f06-8595-5f971ca0119a' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.appointments.get({ appointmentId: '10bdff89-2f4f-4f06-8595-5f971ca0119a' });

Response body

AppointmentResponse
{
  "data": {
    "id": "10bdff89-2f4f-4f06-8595-5f971ca0119a",
    "customer_id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
    "status": "confirmed",
    "scheduled_start": "2026-03-24T13:00:00.000Z",
    "scheduled_end": "2026-03-24T15:00:00.000Z",
    "assigned_team_member_ids": [
      "95cb6a1e-3792-451a-b905-011946edc191"
    ]
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 404 Not found · 429 Rate limited · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Malformed query or request payload.

Usually happens when:
The request sends malformed query/body data.
What to do:
Validate request fields against the contract examples before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Missing plan access, capability, IP access, or scopes for this request.

Usually happens when:
The key is valid but the company’s plan does not include this (reads need a paid plan, writes need Premium or above), API access is suspended, the key IP allowlist does not match, or the key lacks the required scope.
What to do:
Upgrade the plan for writes, adjust the key IP allowlist, or create a key with the required scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

404 Not found

Requested appointment record was not found.

Usually happens when:
The requested `appointmentId` does not exist in the active organization.
What to do:
Re-list appointments for the active org before retrying the direct fetch.
Error response
{
  "error": "We couldn't find that customer. Check its ID and try again."
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle request volume and avoid immediate fan-out retries from parallel workers.
Retry guidance:
Honor the `Retry-After` header and use exponential backoff for repeated limits.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this request.
What to do:
Check API availability, then retry with backoff.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while reading or serializing the resource.
What to do:
Log the contract version and response body, then retry once before escalating to Asteri support.
Error response
{
  "error": "We couldn't load customers. Try again."
}
View appointments schemas and field examples

Appointment

Public representation of a scheduled service appointment.

Appointment fields
FieldTypeRequiredDescription
iduuidYesStable appointment identifier.
customer_iduuidYesCustomer associated with the appointment.
statusAppointmentStatusYesCurrent lifecycle status.
scheduled_startdatetimeYesPlanned appointment start timestamp.
scheduled_enddatetimeYesPlanned appointment end timestamp.
assigned_team_member_idsuuid[]YesTechnician/team assignments for the appointment.
Example Appointment
{
  "id": "10bdff89-2f4f-4f06-8595-5f971ca0119a",
  "customer_id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
  "status": "confirmed",
  "scheduled_start": "2026-03-24T13:00:00.000Z",
  "scheduled_end": "2026-03-24T15:00:00.000Z",
  "assigned_team_member_ids": [
    "95cb6a1e-3792-451a-b905-011946edc191"
  ]
}

AppointmentUpsertRequest

Payload for creating or updating appointments.

AppointmentUpsertRequest fields
FieldTypeRequiredDescription
customer_iduuidYesCustomer that owns the scheduled work.
scheduled_startdatetimeYesAppointment start timestamp.
scheduled_enddatetimeYesAppointment end timestamp.
assigned_team_member_idsuuid[]NoAssigned technicians or team members.
Example AppointmentUpsertRequest
{
  "customer_id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
  "scheduled_start": "2026-03-24T13:00:00.000Z",
  "scheduled_end": "2026-03-24T15:00:00.000Z",
  "assigned_team_member_ids": [
    "95cb6a1e-3792-451a-b905-011946edc191"
  ]
}

AppointmentListResponse

Response body for appointment collection endpoints.

AppointmentListResponse fields
FieldTypeRequiredDescription
dataAppointment[]YesAppointments matching filters.
paginationPaginationYesList metadata for page traversal.
Example AppointmentListResponse
{
  "data": [
    {
      "id": "10bdff89-2f4f-4f06-8595-5f971ca0119a",
      "customer_id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
      "status": "confirmed",
      "scheduled_start": "2026-03-24T13:00:00.000Z",
      "scheduled_end": "2026-03-24T15:00:00.000Z",
      "assigned_team_member_ids": [
        "95cb6a1e-3792-451a-b905-011946edc191"
      ]
    }
  ],
  "pagination": {
    "count": 2,
    "limit": 25,
    "next_cursor": null
  }
}

Job visits

Independent workdays and phases within one commercial job.

/api/public/v1/job-visits
GET
/api/public/v1/job-visitsList the visit work plan and per-visit assignments for one job.

Required permissions

read:appointments

Success:200

Request parameters

jobIdQuery · uuid · Required
The parent job whose visits should be listed.

Usage

cURL
curl -X GET \
  'http://localhost:3000/api/public/v1/job-visits?jobId=8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.jobVisits.list({ jobId: '8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3' });

Response body

JobVisitListResponse
{
  "data": [
    {
      "id": "6fa9f98b-71ce-4a36-9532-9ea85ab2362a",
      "job_id": "10bdff89-2f4f-4f06-8595-5f971ca0119a",
      "sequence": 2,
      "status": "confirmed",
      "scheduled_start": "2026-06-02T13:00:00.000Z",
      "scheduled_end": "2026-06-02T17:00:00.000Z",
      "version": 3
    }
  ]
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 429 Rate limited · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Malformed query or request payload.

Usually happens when:
The request sends malformed query/body data.
What to do:
Validate request fields against the contract examples before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Missing plan access, capability, IP access, or scopes for this request.

Usually happens when:
The key is valid but the company’s plan does not include this (reads need a paid plan, writes need Premium or above), API access is suspended, the key IP allowlist does not match, or the key lacks the required scope.
What to do:
Upgrade the plan for writes, adjust the key IP allowlist, or create a key with the required scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle request volume and avoid immediate fan-out retries from parallel workers.
Retry guidance:
Honor the `Retry-After` header and use exponential backoff for repeated limits.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this request.
What to do:
Check API availability, then retry with backoff.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while reading or serializing the resource.
What to do:
Log the contract version and response body, then retry once before escalating to Asteri support.
Error response
{
  "error": "We couldn't load customers. Try again."
}
POST
/api/public/v1/job-visitsAtomically add one or more visits to an existing job.

Required permissions

write:appointments

Success:200201

Request parameters

Idempotency-KeyHeader · string · Required
A unique key for safe retries of this add request.

Usage

cURL
curl -X POST \
  'http://localhost:3000/api/public/v1/job-visits' \
  -H "Authorization: Bearer ast_live_your_key" \
  -H "Idempotency-Key: partner-request-001" \
  -H "Content-Type: application/json" \
  -d '{
  "job_id": "10bdff89-2f4f-4f06-8595-5f971ca0119a",
  "expected_plan_version": 2,
  "visits": [
    {
      "sequence": 2,
      "label": "Second service day",
      "required": true,
      "status": "scheduled",
      "schedule": {
        "kind": "exact_time",
        "businessDate": "2026-06-02",
        "scheduledStartAt": "2026-06-02T13:00:00.000Z",
        "scheduledEndAt": "2026-06-02T17:00:00.000Z",
        "startTime": "09:00:00",
        "endTime": "13:00:00",
        "timeZone": "America/New_York",
        "customCapacityWindowIds": [],
        "capacityReservations": []
      },
      "crews": [],
      "assignments": [],
      "resources": []
    }
  ]
}'
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.jobVisits.add({
  "job_id": "10bdff89-2f4f-4f06-8595-5f971ca0119a",
  "expected_plan_version": 2,
  "visits": [
    {
      "sequence": 2,
      "label": "Second service day",
      "required": true,
      "status": "scheduled",
      "schedule": {
        "kind": "exact_time",
        "businessDate": "2026-06-02",
        "scheduledStartAt": "2026-06-02T13:00:00.000Z",
        "scheduledEndAt": "2026-06-02T17:00:00.000Z",
        "startTime": "09:00:00",
        "endTime": "13:00:00",
        "timeZone": "America/New_York",
        "customCapacityWindowIds": [],
        "capacityReservations": []
      },
      "crews": [],
      "assignments": [],
      "resources": []
    }
  ]
}, { idempotencyKey: 'partner-request-001' });

Request body

Atomic request to append independently scheduled visits to an existing job.

AddJobVisitsRequest
{
  "job_id": "10bdff89-2f4f-4f06-8595-5f971ca0119a",
  "expected_plan_version": 2,
  "visits": [
    {
      "sequence": 2,
      "label": "Second service day",
      "required": true,
      "status": "scheduled",
      "schedule": {
        "kind": "exact_time",
        "businessDate": "2026-06-02",
        "scheduledStartAt": "2026-06-02T13:00:00.000Z",
        "scheduledEndAt": "2026-06-02T17:00:00.000Z",
        "startTime": "09:00:00",
        "endTime": "13:00:00",
        "timeZone": "America/New_York",
        "customCapacityWindowIds": [],
        "capacityReservations": []
      },
      "crews": [],
      "assignments": [],
      "resources": []
    }
  ]
}
View request fields
AddJobVisitsRequest fields
FieldTypeRequiredDescription
job_iduuidYesParent job that receives the visits.
expected_plan_versionintegerYesCurrent parent visit-plan version used for conflict detection.
visitsJobVisitInput[]YesBounded visit collection. The write is atomic and requires an Idempotency-Key header.

Response body

AddJobVisitsResponse
{
  "data": {
    "job_id": "10bdff89-2f4f-4f06-8595-5f971ca0119a",
    "visit_ids": [
      "6fa9f98b-71ce-4a36-9532-9ea85ab2362a"
    ],
    "visit_plan_version": 2
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 429 Rate limited · 413 Request too large · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Invalid request payload.

Usually happens when:
The request body is missing required fields or includes invalid field values.
What to do:
Validate the payload against the generated request schema and fix any `issues` entries before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Creating and changing records needs the Premium plan or above.

Usually happens when:
The company plan includes reading through the API but not writing, or the key lacks the write scope.
What to do:
Upgrade to Premium in Subscription settings, or create a key with the needed write scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle mutation throughput and avoid blind retries from queues or worker pools.
Retry guidance:
Honor the `Retry-After` header and replay writes only after the relevant limit window resets.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

413 Request too large

The JSON request body exceeds this route’s byte limit.

Usually happens when:
The request body is larger than this write route accepts.
What to do:
Reduce the request body and try again.
Error response
{
  "error": "The request is too large. Send a smaller JSON body and try again."
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this write.
What to do:
Check API availability, then retry with the same idempotency key when applicable.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while validating or persisting the mutation.
What to do:
Capture the response payload and contract version, then retry once before escalating.
Error response
{
  "error": "We couldn't load customers. Try again."
}
View job visits schemas and field examples

JobVisit

One independently scheduled attendance block within a commercial job.

JobVisit fields
FieldTypeRequiredDescription
iduuidYesStable visit identifier used by timers, tracking, artifacts, and calendar events.
job_iduuidYesParent commercial job.
sequenceintegerYesOne-based position in the work plan.
statusJobVisitStatusYesLifecycle state for this visit only.
scheduled_startdatetime | nullYesVisit start instant. Null for anytime or schedule-later visits.
scheduled_enddatetime | nullYesVisit end instant.
versionintegerYesOptimistic concurrency version for visit edits.
Example JobVisit
{
  "id": "6fa9f98b-71ce-4a36-9532-9ea85ab2362a",
  "job_id": "10bdff89-2f4f-4f06-8595-5f971ca0119a",
  "sequence": 2,
  "status": "confirmed",
  "scheduled_start": "2026-06-02T13:00:00.000Z",
  "scheduled_end": "2026-06-02T17:00:00.000Z",
  "version": 3
}

AddJobVisitsRequest

Atomic request to append independently scheduled visits to an existing job.

AddJobVisitsRequest fields
FieldTypeRequiredDescription
job_iduuidYesParent job that receives the visits.
expected_plan_versionintegerYesCurrent parent visit-plan version used for conflict detection.
visitsJobVisitInput[]YesBounded visit collection. The write is atomic and requires an Idempotency-Key header.
Example AddJobVisitsRequest
{
  "job_id": "10bdff89-2f4f-4f06-8595-5f971ca0119a",
  "expected_plan_version": 2,
  "visits": [
    {
      "sequence": 2,
      "label": "Second service day",
      "required": true,
      "status": "scheduled",
      "schedule": {
        "kind": "exact_time",
        "businessDate": "2026-06-02",
        "scheduledStartAt": "2026-06-02T13:00:00.000Z",
        "scheduledEndAt": "2026-06-02T17:00:00.000Z",
        "startTime": "09:00:00",
        "endTime": "13:00:00",
        "timeZone": "America/New_York",
        "customCapacityWindowIds": [],
        "capacityReservations": []
      },
      "crews": [],
      "assignments": [],
      "resources": []
    }
  ]
}

Estimates

Quote creation, revision, sending, and approval lifecycle for service revenue flows.

/api/public/v1/estimates
GET
/api/public/v1/estimatesList estimates with status, customer, and date-based filtering.

Required permissions

read:estimates

Success:200

Request parameters

limitQuery · integer · Optional
Maximum records per page, from 1 to 100 (default 25).
offsetQuery · integer · Optional
Number of matching records to skip.
statusQuery · string · Optional
Filter by estimate status.
customerIdQuery · uuid · Optional
Filter by customer ID.
searchQuery · string · Optional
Filter by estimate number or title.

Usage

cURL
curl -X GET \
  'http://localhost:3000/api/public/v1/estimates' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.estimates.list();

Response body

EstimateListResponse
{
  "data": [
    {
      "id": "97e08590-a443-4725-aa47-7e3cc767a28d",
      "customer_id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
      "status": "sent",
      "currency": "USD",
      "subtotal_amount": 1825,
      "tax_amount": 125,
      "total_amount": 1950,
      "tax_resolution_status": "resolved",
      "line_items": [
        {
          "id": "11111111-1111-4111-8111-111111111111",
          "line_kind": "product",
          "parent_item_id": null,
          "inventory_product_id": "22222222-2222-4222-8222-222222222222",
          "name": "Neutral cleaner",
          "description": null,
          "quantity": 1,
          "unit": "bottle",
          "unit_price": 18.5,
          "discount_amount": 0,
          "subtotal_amount": 18.5,
          "taxable": true,
          "product_snapshot": {
            "schema_version": 1,
            "source_type": "inventory_product",
            "source_id": "22222222-2222-4222-8222-222222222222",
            "name": "Neutral cleaner",
            "sku": "CLEAN-1",
            "unit": "bottle",
            "unit_price": 18.5,
            "currency": "USD",
            "taxable": true
          },
          "tax_components": [
            {
              "name": "District sales tax",
              "jurisdiction": "DC",
              "rate": 6,
              "taxable_base": 18.5,
              "amount": 1.11,
              "is_compound": false,
              "sort_order": 0
            }
          ]
        }
      ]
    }
  ],
  "pagination": {
    "count": 2,
    "limit": 25,
    "next_cursor": null
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 429 Rate limited · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Malformed query or request payload.

Usually happens when:
The request sends malformed query/body data.
What to do:
Validate request fields against the contract examples before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Missing plan access, capability, IP access, or scopes for this request.

Usually happens when:
The key is valid but the company’s plan does not include this (reads need a paid plan, writes need Premium or above), API access is suspended, the key IP allowlist does not match, or the key lacks the required scope.
What to do:
Upgrade the plan for writes, adjust the key IP allowlist, or create a key with the required scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle request volume and avoid immediate fan-out retries from parallel workers.
Retry guidance:
Honor the `Retry-After` header and use exponential backoff for repeated limits.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this request.
What to do:
Check API availability, then retry with backoff.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while reading or serializing the resource.
What to do:
Log the contract version and response body, then retry once before escalating to Asteri support.
Error response
{
  "error": "We couldn't load customers. Try again."
}
POST
/api/public/v1/estimatesCreate a new estimate with service lines and pricing metadata.

Required permissions

write:estimates

Success:201

Request parameters

Idempotency-KeyHeader · string · Required
A unique key for safe retries of this create request.

Usage

cURL
curl -X POST \
  'http://localhost:3000/api/public/v1/estimates' \
  -H "Authorization: Bearer ast_live_your_key" \
  -H "Idempotency-Key: partner-request-001" \
  -H "Content-Type: application/json" \
  -d '{
  "customer_id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
  "service_address_id": null,
  "title": "Quarterly hood cleaning",
  "currency": "USD",
  "line_items": [
    {
      "line_kind": "custom",
      "name": "Quarterly hood cleaning",
      "quantity": 1,
      "unit_price": 1825
    }
  ]
}'
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.estimates.create({
  "customer_id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
  "service_address_id": null,
  "title": "Quarterly hood cleaning",
  "currency": "USD",
  "line_items": [
    {
      "line_kind": "custom",
      "name": "Quarterly hood cleaning",
      "quantity": 1,
      "unit_price": 1825
    }
  ]
}, { idempotencyKey: 'partner-request-001' });

Request body

Payload for creating an estimate.

CreateEstimateRequest
{
  "customer_id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
  "service_address_id": null,
  "title": "Quarterly hood cleaning",
  "currency": "USD",
  "line_items": [
    {
      "line_kind": "custom",
      "name": "Quarterly hood cleaning",
      "quantity": 1,
      "unit_price": 1825
    }
  ]
}
View request fields
CreateEstimateRequest fields
FieldTypeRequiredDescription
customer_iduuidYesCustomer receiving the estimate.
service_address_iduuid | nullNoOrganization-owned customer address used to resolve and save estimate tax evidence.
titlestringNoOptional customer-facing estimate title.
currencystringYesISO currency code.
line_itemsEstimateCreateLine[]YesIndependent estimate lines. Product lines name an organization-owned inventory_product_id; parent_client_item_id may attach a product beneath another line from the same request.

Response body

EstimateResponse
{
  "data": {
    "id": "97e08590-a443-4725-aa47-7e3cc767a28d",
    "customer_id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
    "status": "sent",
    "currency": "USD",
    "subtotal_amount": 1825,
    "tax_amount": 125,
    "total_amount": 1950,
    "tax_resolution_status": "resolved",
    "line_items": [
      {
        "id": "11111111-1111-4111-8111-111111111111",
        "line_kind": "product",
        "parent_item_id": null,
        "inventory_product_id": "22222222-2222-4222-8222-222222222222",
        "name": "Neutral cleaner",
        "description": null,
        "quantity": 1,
        "unit": "bottle",
        "unit_price": 18.5,
        "discount_amount": 0,
        "subtotal_amount": 18.5,
        "taxable": true,
        "product_snapshot": {
          "schema_version": 1,
          "source_type": "inventory_product",
          "source_id": "22222222-2222-4222-8222-222222222222",
          "name": "Neutral cleaner",
          "sku": "CLEAN-1",
          "unit": "bottle",
          "unit_price": 18.5,
          "currency": "USD",
          "taxable": true
        },
        "tax_components": [
          {
            "name": "District sales tax",
            "jurisdiction": "DC",
            "rate": 6,
            "taxable_base": 18.5,
            "amount": 1.11,
            "is_compound": false,
            "sort_order": 0
          }
        ]
      }
    ]
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 404 Not found · 429 Rate limited · 413 Request too large · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Invalid request payload.

Usually happens when:
The request body is missing required fields or includes invalid field values.
What to do:
Validate the payload against the generated request schema and fix any `issues` entries before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Creating and changing records needs the Premium plan or above.

Usually happens when:
The company plan includes reading through the API but not writing, or the key lacks the write scope.
What to do:
Upgrade to Premium in Subscription settings, or create a key with the needed write scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

404 Not found

Referenced customer record was not found.

Usually happens when:
The `customer_id` on the estimate create payload is not visible in the active organization.
What to do:
Create or fetch the customer first, then retry the estimate mutation with that id.
Error response
{
  "error": "We couldn't find that customer. Check its ID and try again."
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle mutation throughput and avoid blind retries from queues or worker pools.
Retry guidance:
Honor the `Retry-After` header and replay writes only after the relevant limit window resets.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

413 Request too large

The JSON request body exceeds this route’s byte limit.

Usually happens when:
The request body is larger than this write route accepts.
What to do:
Reduce the request body and try again.
Error response
{
  "error": "The request is too large. Send a smaller JSON body and try again."
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this write.
What to do:
Check API availability, then retry with the same idempotency key when applicable.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while validating or persisting the mutation.
What to do:
Capture the response payload and contract version, then retry once before escalating.
Error response
{
  "error": "We couldn't load customers. Try again."
}
GET
/api/public/v1/estimates/{estimateId}Retrieve one estimate and current pricing status.

Required permissions

read:estimates

Success:200

Usage

cURL
curl -X GET \
  'http://localhost:3000/api/public/v1/estimates/97e08590-a443-4725-aa47-7e3cc767a28d' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.estimates.get({ estimateId: '97e08590-a443-4725-aa47-7e3cc767a28d' });

Response body

EstimateResponse
{
  "data": {
    "id": "97e08590-a443-4725-aa47-7e3cc767a28d",
    "customer_id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
    "status": "sent",
    "currency": "USD",
    "subtotal_amount": 1825,
    "tax_amount": 125,
    "total_amount": 1950,
    "tax_resolution_status": "resolved",
    "line_items": [
      {
        "id": "11111111-1111-4111-8111-111111111111",
        "line_kind": "product",
        "parent_item_id": null,
        "inventory_product_id": "22222222-2222-4222-8222-222222222222",
        "name": "Neutral cleaner",
        "description": null,
        "quantity": 1,
        "unit": "bottle",
        "unit_price": 18.5,
        "discount_amount": 0,
        "subtotal_amount": 18.5,
        "taxable": true,
        "product_snapshot": {
          "schema_version": 1,
          "source_type": "inventory_product",
          "source_id": "22222222-2222-4222-8222-222222222222",
          "name": "Neutral cleaner",
          "sku": "CLEAN-1",
          "unit": "bottle",
          "unit_price": 18.5,
          "currency": "USD",
          "taxable": true
        },
        "tax_components": [
          {
            "name": "District sales tax",
            "jurisdiction": "DC",
            "rate": 6,
            "taxable_base": 18.5,
            "amount": 1.11,
            "is_compound": false,
            "sort_order": 0
          }
        ]
      }
    ]
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 404 Not found · 429 Rate limited · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Malformed query or request payload.

Usually happens when:
The request sends malformed query/body data.
What to do:
Validate request fields against the contract examples before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Missing plan access, capability, IP access, or scopes for this request.

Usually happens when:
The key is valid but the company’s plan does not include this (reads need a paid plan, writes need Premium or above), API access is suspended, the key IP allowlist does not match, or the key lacks the required scope.
What to do:
Upgrade the plan for writes, adjust the key IP allowlist, or create a key with the required scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

404 Not found

Requested estimate record was not found.

Usually happens when:
The requested `estimateId` does not exist in the active organization.
What to do:
Re-list estimates or verify the estimate belongs to the organization bound to the API key.
Error response
{
  "error": "We couldn't find that customer. Check its ID and try again."
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle request volume and avoid immediate fan-out retries from parallel workers.
Retry guidance:
Honor the `Retry-After` header and use exponential backoff for repeated limits.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this request.
What to do:
Check API availability, then retry with backoff.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while reading or serializing the resource.
What to do:
Log the contract version and response body, then retry once before escalating to Asteri support.
Error response
{
  "error": "We couldn't load customers. Try again."
}
POST
/api/public/v1/estimates/{estimateId}/sendQueue email delivery of an immutable published estimate revision.

Required permissions

write:estimates

Success:202

Usage

cURL
curl -X POST \
  'http://localhost:3000/api/public/v1/estimates/97e08590-a443-4725-aa47-7e3cc767a28d/send' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.estimates.send({ estimateId: '97e08590-a443-4725-aa47-7e3cc767a28d' });

Response body

EstimateSendResponse
{
  "data": {
    "estimate_id": "97e08590-a443-4725-aa47-7e3cc767a28d",
    "status": "queued"
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 404 Not found · 409 Conflict · 412 Precondition failed · 429 Rate limited · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Invalid request payload.

Usually happens when:
The request body is missing required fields or includes invalid field values.
What to do:
Validate the payload against the generated request schema and fix any `issues` entries before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Creating and changing records needs the Premium plan or above.

Usually happens when:
The company plan includes reading through the API but not writing, or the key lacks the write scope.
What to do:
Upgrade to Premium in Subscription settings, or create a key with the needed write scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

404 Not found

Requested estimate record was not found.

Usually happens when:
The requested `estimateId` does not exist in the active organization.
What to do:
Re-fetch the estimate before retrying send, especially after local cache invalidation or org changes.
Error response
{
  "error": "We couldn't find that customer. Check its ID and try again."
}

409 Conflict

The estimate has a conflicting revision or pending delivery.

Usually happens when:
A numbered published revision is missing or invalid, a draft revision is pending, or another delivery is already scheduled.
What to do:
Refresh the estimate state and resolve or cancel the conflicting revision or scheduled delivery before retrying.
Error response
{
  "error": "A customer already uses this email. Use a different email or open the existing customer.",
  "field": "email"
}

412 Precondition failed

The estimate or API key sender is not ready for email delivery.

Usually happens when:
Sales tax is unresolved, the customer has no email address, or the API key creator is no longer an active member of the organization.
What to do:
Resolve tax, add the customer email, or create a replacement key from an active team member, then retry.
Error response
{
  "error": "Configure or manually resolve sales tax before publishing."
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle mutation throughput and avoid blind retries from queues or worker pools.
Retry guidance:
Honor the `Retry-After` header and replay writes only after the relevant limit window resets.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this write.
What to do:
Check API availability, then retry with the same idempotency key when applicable.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while validating or persisting the mutation.
What to do:
Capture the response payload and contract version, then retry once before escalating.
Error response
{
  "error": "We couldn't load customers. Try again."
}
View estimates schemas and field examples

Estimate

Public representation of an estimate or quote.

Estimate fields
FieldTypeRequiredDescription
iduuidYesStable estimate identifier.
customer_iduuidYesCustomer associated with the estimate.
statusEstimateStatusYesEstimate lifecycle state.
currencystringYesISO currency code.
subtotal_amountnumberYesSubtotal before taxes/fees.
tax_amountnumberYesSaved tax total from the estimate pricing evidence.
total_amountnumberYesFinal estimate amount.
tax_resolution_statusstring | nullYesSaved tax resolution status. Unresolved values must be corrected before sending.
line_itemsEstimateLine[]YesIndependent service, product, add-on, custom, and adjustment lines with public-safe catalog and saved tax evidence.
Example Estimate
{
  "id": "97e08590-a443-4725-aa47-7e3cc767a28d",
  "customer_id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
  "status": "sent",
  "currency": "USD",
  "subtotal_amount": 1825,
  "tax_amount": 125,
  "total_amount": 1950,
  "tax_resolution_status": "resolved",
  "line_items": [
    {
      "id": "11111111-1111-4111-8111-111111111111",
      "line_kind": "product",
      "parent_item_id": null,
      "inventory_product_id": "22222222-2222-4222-8222-222222222222",
      "name": "Neutral cleaner",
      "description": null,
      "quantity": 1,
      "unit": "bottle",
      "unit_price": 18.5,
      "discount_amount": 0,
      "subtotal_amount": 18.5,
      "taxable": true,
      "product_snapshot": {
        "schema_version": 1,
        "source_type": "inventory_product",
        "source_id": "22222222-2222-4222-8222-222222222222",
        "name": "Neutral cleaner",
        "sku": "CLEAN-1",
        "unit": "bottle",
        "unit_price": 18.5,
        "currency": "USD",
        "taxable": true
      },
      "tax_components": [
        {
          "name": "District sales tax",
          "jurisdiction": "DC",
          "rate": 6,
          "taxable_base": 18.5,
          "amount": 1.11,
          "is_compound": false,
          "sort_order": 0
        }
      ]
    }
  ]
}

EstimateCreateLine

One custom or inventory-product line in an estimate creation request.

EstimateCreateLine fields
FieldTypeRequiredDescription
client_item_idstringNoOptional request-local identifier used to attach child lines.
parent_client_item_idstring | nullNoRequest-local parent identifier for an attached product line.
line_kind"custom" | "product"NoWhether the line is custom-priced or sourced from inventory.
inventory_product_iduuid | nullNoOrganization-owned inventory product required for product lines.
namestringNoCustomer-facing line name. Required for custom lines.
descriptionstring | nullNoOptional customer-facing line description.
quantitynumberYesPositive quantity for this line.
unitstringNoOptional unit of measure.
unit_pricenumberNoOptional nonnegative unit price. Product lines may resolve it from the catalog.
taxablebooleanNoOptional line-level taxability override.
Example EstimateCreateLine
{
  "line_kind": "custom",
  "name": "Quarterly hood cleaning",
  "quantity": 1,
  "unit_price": 1825
}

CreateEstimateRequest

Payload for creating an estimate.

CreateEstimateRequest fields
FieldTypeRequiredDescription
customer_iduuidYesCustomer receiving the estimate.
service_address_iduuid | nullNoOrganization-owned customer address used to resolve and save estimate tax evidence.
titlestringNoOptional customer-facing estimate title.
currencystringYesISO currency code.
line_itemsEstimateCreateLine[]YesIndependent estimate lines. Product lines name an organization-owned inventory_product_id; parent_client_item_id may attach a product beneath another line from the same request.
Example CreateEstimateRequest
{
  "customer_id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
  "service_address_id": null,
  "title": "Quarterly hood cleaning",
  "currency": "USD",
  "line_items": [
    {
      "line_kind": "custom",
      "name": "Quarterly hood cleaning",
      "quantity": 1,
      "unit_price": 1825
    }
  ]
}

EstimateResponse

Single-estimate response body.

EstimateResponse fields
FieldTypeRequiredDescription
dataEstimateYesRequested estimate object.
Example EstimateResponse
{
  "data": {
    "id": "97e08590-a443-4725-aa47-7e3cc767a28d",
    "customer_id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
    "status": "sent",
    "currency": "USD",
    "subtotal_amount": 1825,
    "tax_amount": 125,
    "total_amount": 1950,
    "tax_resolution_status": "resolved",
    "line_items": [
      {
        "id": "11111111-1111-4111-8111-111111111111",
        "line_kind": "product",
        "parent_item_id": null,
        "inventory_product_id": "22222222-2222-4222-8222-222222222222",
        "name": "Neutral cleaner",
        "description": null,
        "quantity": 1,
        "unit": "bottle",
        "unit_price": 18.5,
        "discount_amount": 0,
        "subtotal_amount": 18.5,
        "taxable": true,
        "product_snapshot": {
          "schema_version": 1,
          "source_type": "inventory_product",
          "source_id": "22222222-2222-4222-8222-222222222222",
          "name": "Neutral cleaner",
          "sku": "CLEAN-1",
          "unit": "bottle",
          "unit_price": 18.5,
          "currency": "USD",
          "taxable": true
        },
        "tax_components": [
          {
            "name": "District sales tax",
            "jurisdiction": "DC",
            "rate": 6,
            "taxable_base": 18.5,
            "amount": 1.11,
            "is_compound": false,
            "sort_order": 0
          }
        ]
      }
    ]
  }
}

Invoices

Billing state, payment readiness, and accounting-system handoff.

/api/public/v1/invoices
GET
/api/public/v1/invoicesList invoices by customer, status, or date range.

Required permissions

read:invoices

Success:200

Request parameters

limitQuery · integer · Optional
Maximum records per page, from 1 to 100 (default 25).
offsetQuery · integer · Optional
Number of matching records to skip.
statusQuery · string · Optional
Filter by invoice status.
customerIdQuery · uuid · Optional
Filter by customer ID.
startDateQuery · date · Optional
Earliest invoice date.
endDateQuery · date · Optional
Latest invoice date.

Usage

cURL
curl -X GET \
  'http://localhost:3000/api/public/v1/invoices' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.invoices.list();

Response body

InvoiceListResponse
{
  "data": [
    {
      "id": "4dd2dc3d-51a9-4e91-a225-67d1d69cf42e",
      "customer_id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
      "status": "sent",
      "payment_status": "pending",
      "amount_paid": 1750,
      "balance_due": 200,
      "customer_credit": 0,
      "payment_state": "partial",
      "is_overdue": false,
      "can_collect_payment": true,
      "should_render_payment_link": true,
      "due_date": "2026-04-15"
    }
  ],
  "pagination": {
    "count": 2,
    "limit": 25,
    "next_cursor": null
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 429 Rate limited · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Malformed query or request payload.

Usually happens when:
The request sends malformed query/body data.
What to do:
Validate request fields against the contract examples before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Missing plan access, capability, IP access, or scopes for this request.

Usually happens when:
The key is valid but the company’s plan does not include this (reads need a paid plan, writes need Premium or above), API access is suspended, the key IP allowlist does not match, or the key lacks the required scope.
What to do:
Upgrade the plan for writes, adjust the key IP allowlist, or create a key with the required scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle request volume and avoid immediate fan-out retries from parallel workers.
Retry guidance:
Honor the `Retry-After` header and use exponential backoff for repeated limits.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this request.
What to do:
Check API availability, then retry with backoff.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while reading or serializing the resource.
What to do:
Log the contract version and response body, then retry once before escalating to Asteri support.
Error response
{
  "error": "We couldn't load customers. Try again."
}
GET
/api/public/v1/invoices/{invoiceId}Retrieve one invoice and current balance/payment metadata.

Required permissions

read:invoices

Success:200

Usage

cURL
curl -X GET \
  'http://localhost:3000/api/public/v1/invoices/4dd2dc3d-51a9-4e91-a225-67d1d69cf42e' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.invoices.get({ invoiceId: '4dd2dc3d-51a9-4e91-a225-67d1d69cf42e' });

Response body

InvoiceResponse
{
  "data": {
    "id": "4dd2dc3d-51a9-4e91-a225-67d1d69cf42e",
    "customer_id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
    "status": "sent",
    "payment_status": "pending",
    "amount_paid": 1750,
    "balance_due": 200,
    "customer_credit": 0,
    "payment_state": "partial",
    "is_overdue": false,
    "can_collect_payment": true,
    "should_render_payment_link": true,
    "due_date": "2026-04-15"
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 404 Not found · 429 Rate limited · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Malformed query or request payload.

Usually happens when:
The request sends malformed query/body data.
What to do:
Validate request fields against the contract examples before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Missing plan access, capability, IP access, or scopes for this request.

Usually happens when:
The key is valid but the company’s plan does not include this (reads need a paid plan, writes need Premium or above), API access is suspended, the key IP allowlist does not match, or the key lacks the required scope.
What to do:
Upgrade the plan for writes, adjust the key IP allowlist, or create a key with the required scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

404 Not found

Requested invoice record was not found.

Usually happens when:
The requested `invoiceId` does not exist in the active organization.
What to do:
Refresh invoice ids from a list call or confirm the invoice belongs to the org in your request.
Error response
{
  "error": "We couldn't find that customer. Check its ID and try again."
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle request volume and avoid immediate fan-out retries from parallel workers.
Retry guidance:
Honor the `Retry-After` header and use exponential backoff for repeated limits.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this request.
What to do:
Check API availability, then retry with backoff.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while reading or serializing the resource.
What to do:
Log the contract version and response body, then retry once before escalating to Asteri support.
Error response
{
  "error": "We couldn't load customers. Try again."
}
View invoices schemas and field examples

Invoice

Public representation of an invoice record.

Invoice fields
FieldTypeRequiredDescription
iduuidYesStable invoice identifier.
customer_iduuidYesCustomer billed by this invoice.
statusInvoiceStatusYesInvoice lifecycle state.
payment_statusPaymentStatusYesPayment collection state for the invoice.
amount_paidnumberYesEffective principal settled from the payment ledger.
balance_duenumberYesCanonical amount still due; never negative.
customer_creditnumberYesLedger principal above the current invoice total.
payment_stateInvoicePaymentStateYesCanonical settlement state derived from the payment ledger.
is_overduebooleanYesWhether a positive balance is overdue in the organization business date.
can_collect_paymentbooleanYesWhether a new payment may be collected now.
should_render_payment_linkbooleanYesWhether customer-facing clients should render a payment link.
due_datedateYesInvoice due date.
Example Invoice
{
  "id": "4dd2dc3d-51a9-4e91-a225-67d1d69cf42e",
  "customer_id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
  "status": "sent",
  "payment_status": "pending",
  "amount_paid": 1750,
  "balance_due": 200,
  "customer_credit": 0,
  "payment_state": "partial",
  "is_overdue": false,
  "can_collect_payment": true,
  "should_render_payment_link": true,
  "due_date": "2026-04-15"
}

InvoiceResponse

Single-invoice response body.

InvoiceResponse fields
FieldTypeRequiredDescription
dataInvoiceYesRequested invoice object.
Example InvoiceResponse
{
  "data": {
    "id": "4dd2dc3d-51a9-4e91-a225-67d1d69cf42e",
    "customer_id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
    "status": "sent",
    "payment_status": "pending",
    "amount_paid": 1750,
    "balance_due": 200,
    "customer_credit": 0,
    "payment_state": "partial",
    "is_overdue": false,
    "can_collect_payment": true,
    "should_render_payment_link": true,
    "due_date": "2026-04-15"
  }
}

Payments

Collected payments, invoice linkage, payment methods, and collection-state visibility.

/api/public/v1/payments
GET
/api/public/v1/paymentsList payments by customer, invoice, status, or date range.

Required permissions

read:payments

Success:200

Request parameters

limitQuery · integer · Optional
Maximum records per page, from 1 to 100 (default 25).
offsetQuery · integer · Optional
Number of matching records to skip.
statusQuery · string · Optional
Filter by payment status.
customerIdQuery · uuid · Optional
Filter by customer ID.
invoiceIdQuery · uuid · Optional
Filter by invoice ID.
startDateQuery · date · Optional
Earliest payment date.
endDateQuery · date · Optional
Latest payment date.

Usage

cURL
curl -X GET \
  'http://localhost:3000/api/public/v1/payments' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.payments.list();

Response body

PaymentListResponse
{
  "data": [
    {
      "id": "1fc10a48-2c62-4d17-a7f1-4b1f42cb6c84",
      "customer_id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
      "invoice_id": "4dd2dc3d-51a9-4e91-a225-67d1d69cf42e",
      "amount": 1950,
      "status": "completed",
      "payment_method": "card",
      "payment_date": "2026-03-22",
      "created_at": "2026-03-22T15:40:12.000Z"
    }
  ],
  "pagination": {
    "count": 2,
    "limit": 25,
    "next_cursor": null
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 429 Rate limited · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Malformed query or request payload.

Usually happens when:
The request sends malformed query/body data.
What to do:
Validate request fields against the contract examples before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Missing plan access, capability, IP access, or scopes for this request.

Usually happens when:
The key is valid but the company’s plan does not include this (reads need a paid plan, writes need Premium or above), API access is suspended, the key IP allowlist does not match, or the key lacks the required scope.
What to do:
Upgrade the plan for writes, adjust the key IP allowlist, or create a key with the required scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle request volume and avoid immediate fan-out retries from parallel workers.
Retry guidance:
Honor the `Retry-After` header and use exponential backoff for repeated limits.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this request.
What to do:
Check API availability, then retry with backoff.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while reading or serializing the resource.
What to do:
Log the contract version and response body, then retry once before escalating to Asteri support.
Error response
{
  "error": "We couldn't load customers. Try again."
}
GET
/api/public/v1/payments/{paymentId}Retrieve one payment and its invoice/customer linkage.

Required permissions

read:payments

Success:200

Usage

cURL
curl -X GET \
  'http://localhost:3000/api/public/v1/payments/8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.payments.get({ paymentId: '8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3' });

Response body

PaymentResponse
{
  "data": {
    "id": "1fc10a48-2c62-4d17-a7f1-4b1f42cb6c84",
    "customer_id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
    "invoice_id": "4dd2dc3d-51a9-4e91-a225-67d1d69cf42e",
    "amount": 1950,
    "status": "completed",
    "payment_method": "card",
    "payment_date": "2026-03-22",
    "created_at": "2026-03-22T15:40:12.000Z"
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 404 Not found · 429 Rate limited · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Malformed query or request payload.

Usually happens when:
The request sends malformed query/body data.
What to do:
Validate request fields against the contract examples before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Missing plan access, capability, IP access, or scopes for this request.

Usually happens when:
The key is valid but the company’s plan does not include this (reads need a paid plan, writes need Premium or above), API access is suspended, the key IP allowlist does not match, or the key lacks the required scope.
What to do:
Upgrade the plan for writes, adjust the key IP allowlist, or create a key with the required scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

404 Not found

Requested payment record was not found.

Usually happens when:
The requested `paymentId` does not exist in the active organization.
What to do:
Refresh payments from a list call or confirm the payment belongs to the org context in your request.
Error response
{
  "error": "We couldn't find that customer. Check its ID and try again."
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle request volume and avoid immediate fan-out retries from parallel workers.
Retry guidance:
Honor the `Retry-After` header and use exponential backoff for repeated limits.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this request.
What to do:
Check API availability, then retry with backoff.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while reading or serializing the resource.
What to do:
Log the contract version and response body, then retry once before escalating to Asteri support.
Error response
{
  "error": "We couldn't load customers. Try again."
}
View payments schemas and field examples

Payment

Public representation of a collected or recorded payment.

Payment fields
FieldTypeRequiredDescription
iduuidYesStable payment identifier.
customer_iduuidYesCustomer associated with the payment.
invoice_iduuidNoInvoice associated with the payment when one exists.
amountnumberYesPayment amount in the organization currency.
statusPaymentStatusYesCurrent lifecycle state for the payment.
payment_methodstringYesPayment method type recorded on the payment.
payment_datedateYesDate the payment was collected or recorded.
created_atdatetimeYesISO-8601 timestamp for record creation.
Example Payment
{
  "id": "1fc10a48-2c62-4d17-a7f1-4b1f42cb6c84",
  "customer_id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
  "invoice_id": "4dd2dc3d-51a9-4e91-a225-67d1d69cf42e",
  "amount": 1950,
  "status": "completed",
  "payment_method": "card",
  "payment_date": "2026-03-22",
  "created_at": "2026-03-22T15:40:12.000Z"
}

PaymentListResponse

Collection response for payments.

PaymentListResponse fields
FieldTypeRequiredDescription
dataPayment[]YesPayments matching the current filters.
paginationPaginationYesList metadata for page traversal.
Example PaymentListResponse
{
  "data": [
    {
      "id": "1fc10a48-2c62-4d17-a7f1-4b1f42cb6c84",
      "customer_id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
      "invoice_id": "4dd2dc3d-51a9-4e91-a225-67d1d69cf42e",
      "amount": 1950,
      "status": "completed",
      "payment_method": "card",
      "payment_date": "2026-03-22",
      "created_at": "2026-03-22T15:40:12.000Z"
    }
  ],
  "pagination": {
    "count": 2,
    "limit": 25,
    "next_cursor": null
  }
}

PaymentResponse

Single-payment response body.

PaymentResponse fields
FieldTypeRequiredDescription
dataPaymentYesRequested payment object.
Example PaymentResponse
{
  "data": {
    "id": "1fc10a48-2c62-4d17-a7f1-4b1f42cb6c84",
    "customer_id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
    "invoice_id": "4dd2dc3d-51a9-4e91-a225-67d1d69cf42e",
    "amount": 1950,
    "status": "completed",
    "payment_method": "card",
    "payment_date": "2026-03-22",
    "created_at": "2026-03-22T15:40:12.000Z"
  }
}

Services

Service catalog visibility for pricing, duration, and booking-facing configuration.

/api/public/v1/services
GET
/api/public/v1/servicesList services by active state, catalog visibility, or search query.

Required permissions

read:services

Success:200

Request parameters

limitQuery · integer · Optional
Maximum records per page, from 1 to 100 (default 25).
offsetQuery · integer · Optional
Number of matching records to skip.
activeQuery · boolean · Optional
Filter by active state.
searchQuery · string · Optional
Filter by service name.

Usage

cURL
curl -X GET \
  'http://localhost:3000/api/public/v1/services' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.services.list();

Response body

ServiceListResponse
{
  "data": [
    {
      "id": "10fbd700-2b14-44c8-8b7b-f7786b3d9f74",
      "name": "Kitchen exhaust cleaning",
      "description": "Full hood, duct, and fan cleaning service.",
      "price": 1950,
      "duration_minutes": 180,
      "is_active": true,
      "service_type": "commercial_kitchen",
      "created_at": "2026-03-18T15:40:12.000Z"
    }
  ],
  "pagination": {
    "count": 2,
    "limit": 25,
    "next_cursor": null
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 429 Rate limited · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Malformed query or request payload.

Usually happens when:
The request sends malformed query/body data.
What to do:
Validate request fields against the contract examples before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Missing plan access, capability, IP access, or scopes for this request.

Usually happens when:
The key is valid but the company’s plan does not include this (reads need a paid plan, writes need Premium or above), API access is suspended, the key IP allowlist does not match, or the key lacks the required scope.
What to do:
Upgrade the plan for writes, adjust the key IP allowlist, or create a key with the required scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle request volume and avoid immediate fan-out retries from parallel workers.
Retry guidance:
Honor the `Retry-After` header and use exponential backoff for repeated limits.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this request.
What to do:
Check API availability, then retry with backoff.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while reading or serializing the resource.
What to do:
Log the contract version and response body, then retry once before escalating to Asteri support.
Error response
{
  "error": "We couldn't load customers. Try again."
}
GET
/api/public/v1/services/{serviceId}Retrieve one service and its public catalog metadata.

Required permissions

read:services

Success:200

Usage

cURL
curl -X GET \
  'http://localhost:3000/api/public/v1/services/8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.services.get({ serviceId: '8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3' });

Response body

ServiceResponse
{
  "data": {
    "id": "10fbd700-2b14-44c8-8b7b-f7786b3d9f74",
    "name": "Kitchen exhaust cleaning",
    "description": "Full hood, duct, and fan cleaning service.",
    "price": 1950,
    "duration_minutes": 180,
    "is_active": true,
    "service_type": "commercial_kitchen",
    "created_at": "2026-03-18T15:40:12.000Z"
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 404 Not found · 429 Rate limited · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Malformed query or request payload.

Usually happens when:
The request sends malformed query/body data.
What to do:
Validate request fields against the contract examples before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Missing plan access, capability, IP access, or scopes for this request.

Usually happens when:
The key is valid but the company’s plan does not include this (reads need a paid plan, writes need Premium or above), API access is suspended, the key IP allowlist does not match, or the key lacks the required scope.
What to do:
Upgrade the plan for writes, adjust the key IP allowlist, or create a key with the required scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

404 Not found

Requested service record was not found.

Usually happens when:
The requested `serviceId` does not exist in the active organization.
What to do:
Refresh the service catalog or confirm the service belongs to the org in your request.
Error response
{
  "error": "We couldn't find that customer. Check its ID and try again."
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle request volume and avoid immediate fan-out retries from parallel workers.
Retry guidance:
Honor the `Retry-After` header and use exponential backoff for repeated limits.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this request.
What to do:
Check API availability, then retry with backoff.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while reading or serializing the resource.
What to do:
Log the contract version and response body, then retry once before escalating to Asteri support.
Error response
{
  "error": "We couldn't load customers. Try again."
}
View services schemas and field examples

Service

Public representation of a service catalog item.

Service fields
FieldTypeRequiredDescription
iduuidYesStable service identifier.
namestringYesDisplay name for the service.
descriptionstring | nullYesCustomer-facing service description.
pricenumberYesBase service price.
duration_minutesintegerYesDefault service duration in minutes.
is_activebooleanYesWhether the service is currently active and bookable.
service_typestring | nullYesInternal service type classification when present.
created_atdatetimeYesISO-8601 timestamp for record creation.
Example Service
{
  "id": "10fbd700-2b14-44c8-8b7b-f7786b3d9f74",
  "name": "Kitchen exhaust cleaning",
  "description": "Full hood, duct, and fan cleaning service.",
  "price": 1950,
  "duration_minutes": 180,
  "is_active": true,
  "service_type": "commercial_kitchen",
  "created_at": "2026-03-18T15:40:12.000Z"
}

ServiceListResponse

Collection response for services.

ServiceListResponse fields
FieldTypeRequiredDescription
dataService[]YesServices matching the current filters.
paginationPaginationYesList metadata for page traversal.
Example ServiceListResponse
{
  "data": [
    {
      "id": "10fbd700-2b14-44c8-8b7b-f7786b3d9f74",
      "name": "Kitchen exhaust cleaning",
      "description": "Full hood, duct, and fan cleaning service.",
      "price": 1950,
      "duration_minutes": 180,
      "is_active": true,
      "service_type": "commercial_kitchen",
      "created_at": "2026-03-18T15:40:12.000Z"
    }
  ],
  "pagination": {
    "count": 2,
    "limit": 25,
    "next_cursor": null
  }
}

ServiceResponse

Single-service response body.

ServiceResponse fields
FieldTypeRequiredDescription
dataServiceYesRequested service object.
Example ServiceResponse
{
  "data": {
    "id": "10fbd700-2b14-44c8-8b7b-f7786b3d9f74",
    "name": "Kitchen exhaust cleaning",
    "description": "Full hood, duct, and fan cleaning service.",
    "price": 1950,
    "duration_minutes": 180,
    "is_active": true,
    "service_type": "commercial_kitchen",
    "created_at": "2026-03-18T15:40:12.000Z"
  }
}

Files

Uploaded file records, MIME metadata, processing status, and storage-bucket context.

/api/public/v1/files
GET
/api/public/v1/filesList files by status, content type, or filename search.

Required permissions

read:files

Success:200

Request parameters

limitQuery · integer · Optional
Maximum records per page, from 1 to 100 (default 25).
offsetQuery · integer · Optional
Number of matching records to skip.
statusQuery · string · Optional
Filter by file status.
contentTypeQuery · string · Optional
Filter by MIME type.
searchQuery · string · Optional
Filter by original file name.

Usage

cURL
curl -X GET \
  'http://localhost:3000/api/public/v1/files' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.files.list();

Response body

FileListResponse
{
  "data": [
    {
      "id": "00M6L4J2DT8C8492E03F818F27",
      "original_name": "before-cleaning-photo.jpg",
      "content_type": "image/jpeg",
      "size_bytes": 452301,
      "status": "ready",
      "bucket": "uploads",
      "created_at": "2026-03-18T15:40:12.000Z"
    }
  ],
  "pagination": {
    "count": 2,
    "limit": 25,
    "next_cursor": null
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 429 Rate limited · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Malformed query or request payload.

Usually happens when:
The request sends malformed query/body data.
What to do:
Validate request fields against the contract examples before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Missing plan access, capability, IP access, or scopes for this request.

Usually happens when:
The key is valid but the company’s plan does not include this (reads need a paid plan, writes need Premium or above), API access is suspended, the key IP allowlist does not match, or the key lacks the required scope.
What to do:
Upgrade the plan for writes, adjust the key IP allowlist, or create a key with the required scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle request volume and avoid immediate fan-out retries from parallel workers.
Retry guidance:
Honor the `Retry-After` header and use exponential backoff for repeated limits.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this request.
What to do:
Check API availability, then retry with backoff.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while reading or serializing the resource.
What to do:
Log the contract version and response body, then retry once before escalating to Asteri support.
Error response
{
  "error": "We couldn't load customers. Try again."
}
GET
/api/public/v1/files/{fileId}Retrieve one file record and current metadata state.

Required permissions

read:files

Success:200

Usage

cURL
curl -X GET \
  'http://localhost:3000/api/public/v1/files/00M6L4J2DT8C8492E03F818F27' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.files.get({ fileId: '00M6L4J2DT8C8492E03F818F27' });

Response body

FileResponse
{
  "data": {
    "id": "00M6L4J2DT8C8492E03F818F27",
    "original_name": "before-cleaning-photo.jpg",
    "content_type": "image/jpeg",
    "size_bytes": 452301,
    "status": "ready",
    "bucket": "uploads",
    "created_at": "2026-03-18T15:40:12.000Z"
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 404 Not found · 429 Rate limited · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Malformed query or request payload.

Usually happens when:
The request sends malformed query/body data.
What to do:
Validate request fields against the contract examples before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Missing plan access, capability, IP access, or scopes for this request.

Usually happens when:
The key is valid but the company’s plan does not include this (reads need a paid plan, writes need Premium or above), API access is suspended, the key IP allowlist does not match, or the key lacks the required scope.
What to do:
Upgrade the plan for writes, adjust the key IP allowlist, or create a key with the required scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

404 Not found

Requested file record was not found.

Usually happens when:
The requested `fileId` does not exist in the active organization or is no longer visible.
What to do:
Refresh the file list or confirm the file belongs to the org context in your request.
Error response
{
  "error": "We couldn't find that customer. Check its ID and try again."
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle request volume and avoid immediate fan-out retries from parallel workers.
Retry guidance:
Honor the `Retry-After` header and use exponential backoff for repeated limits.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this request.
What to do:
Check API availability, then retry with backoff.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while reading or serializing the resource.
What to do:
Log the contract version and response body, then retry once before escalating to Asteri support.
Error response
{
  "error": "We couldn't load customers. Try again."
}
View files schemas and field examples

File

Public representation of a file record managed by Asteri.

File fields
FieldTypeRequiredDescription
idstringYesStable file identifier.
original_namestringYesOriginal uploaded file name.
content_typestringYesMIME type recorded for the file.
size_bytesintegerYesFile size in bytes.
statusstringYesCurrent file processing or availability status.
bucketstringYesStorage bucket used for the file.
created_atdatetimeYesISO-8601 timestamp for record creation.
Example File
{
  "id": "00M6L4J2DT8C8492E03F818F27",
  "original_name": "before-cleaning-photo.jpg",
  "content_type": "image/jpeg",
  "size_bytes": 452301,
  "status": "ready",
  "bucket": "uploads",
  "created_at": "2026-03-18T15:40:12.000Z"
}

FileListResponse

Collection response for files.

FileListResponse fields
FieldTypeRequiredDescription
dataFile[]YesFiles matching the current filters.
paginationPaginationYesList metadata for page traversal.
Example FileListResponse
{
  "data": [
    {
      "id": "00M6L4J2DT8C8492E03F818F27",
      "original_name": "before-cleaning-photo.jpg",
      "content_type": "image/jpeg",
      "size_bytes": 452301,
      "status": "ready",
      "bucket": "uploads",
      "created_at": "2026-03-18T15:40:12.000Z"
    }
  ],
  "pagination": {
    "count": 2,
    "limit": 25,
    "next_cursor": null
  }
}

FileResponse

Single-file response body.

FileResponse fields
FieldTypeRequiredDescription
dataFileYesRequested file object.
Example FileResponse
{
  "data": {
    "id": "00M6L4J2DT8C8492E03F818F27",
    "original_name": "before-cleaning-photo.jpg",
    "content_type": "image/jpeg",
    "size_bytes": 452301,
    "status": "ready",
    "bucket": "uploads",
    "created_at": "2026-03-18T15:40:12.000Z"
  }
}

Photos

Job photos and videos: list, read, tag, comment on, read and save markup, import from a link and upload directly.

/api/public/v1/photos
GET
/api/public/v1/photosList photos newest first; filter by job_id, customer_id, tag_id (repeat or comma list), tag_match, media_type, since and until.

Required permissions

read:photos

Success:200

Request parameters

limitQuery · integer · Optional
Maximum records per page, from 1 to 100 (default 25).
cursorQuery · string · Optional
Opaque next_cursor from the previous response.
job_idQuery · uuid · Optional
Filter by job ID.
customer_idQuery · uuid · Optional
Filter by customer ID.
sinceQuery · datetime · Optional
Earliest photo creation time.
untilQuery · datetime · Optional
Latest photo creation time.
tag_idQuery · uuid[] · Optional
Filter by one or more tag IDs.
tag_matchQuery · any | all · Optional
Match any or all listed tags.
media_typeQuery · photo | video · Optional
Filter by media type.

Usage

cURL
curl -X GET \
  'http://localhost:3000/api/public/v1/photos' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.photos.list();

Response body

PhotoListResponse
{
  "data": [
    {
      "id": "0MG4Z8K1QX3F9A7C2D4E6B8A0C",
      "job_id": "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10",
      "job_title": "Roof repair",
      "customer_id": null,
      "customer_name": "Dana Whitfield",
      "media_type": "photo",
      "content_type": "image/jpeg",
      "original_name": "front-elevation.jpg",
      "size_bytes": 1843200,
      "caption": "Flashing lifted at the chimney",
      "stage": "before",
      "captured_at": "2026-09-28T14:05:00.000Z",
      "created_at": "2026-09-28T14:06:12.000Z",
      "coordinates": {
        "lat": 39.3719,
        "lng": -76.4632
      },
      "uploaded_by": {
        "id": "9f2c7e1a-4b3d-4e5f-8a6b-2c1d3e4f5a6b",
        "name": "Chris Lane"
      },
      "tags": [
        {
          "id": "3c9d2e1f-7a4b-4c6d-8e2f-1a3b5c7d9e0f",
          "name": "Roof"
        }
      ],
      "download_url": "https://files.example.com/signed",
      "download_url_expires_at": "2026-09-28T15:06:12.000Z"
    }
  ],
  "pagination": {
    "count": 2,
    "limit": 25,
    "next_cursor": null
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 429 Rate limited · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Malformed query or request payload.

Usually happens when:
The request sends malformed query/body data.
What to do:
Validate request fields against the contract examples before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Missing plan access, capability, IP access, or scopes for this request.

Usually happens when:
The key is valid but the company’s plan does not include this (reads need a paid plan, writes need Premium or above), API access is suspended, the key IP allowlist does not match, or the key lacks the required scope.
What to do:
Upgrade the plan for writes, adjust the key IP allowlist, or create a key with the required scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle request volume and avoid immediate fan-out retries from parallel workers.
Retry guidance:
Honor the `Retry-After` header and use exponential backoff for repeated limits.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this request.
What to do:
Check API availability, then retry with backoff.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while reading or serializing the resource.
What to do:
Log the contract version and response body, then retry once before escalating to Asteri support.
Error response
{
  "error": "We couldn't load customers. Try again."
}
GET
/api/public/v1/photos/{photoId}Read one photo with a download link that lasts an hour.

Required permissions

read:photos

Success:200

Usage

cURL
curl -X GET \
  'http://localhost:3000/api/public/v1/photos/00M6L4J2DT8C8492E03F818F27' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.photos.get({ photoId: '00M6L4J2DT8C8492E03F818F27' });

Response body

PhotoResponse
{
  "data": {
    "id": "0MG4Z8K1QX3F9A7C2D4E6B8A0C",
    "job_id": "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10",
    "job_title": "Roof repair",
    "customer_id": null,
    "customer_name": "Dana Whitfield",
    "media_type": "photo",
    "content_type": "image/jpeg",
    "original_name": "front-elevation.jpg",
    "size_bytes": 1843200,
    "caption": "Flashing lifted at the chimney",
    "stage": "before",
    "captured_at": "2026-09-28T14:05:00.000Z",
    "created_at": "2026-09-28T14:06:12.000Z",
    "coordinates": {
      "lat": 39.3719,
      "lng": -76.4632
    },
    "uploaded_by": {
      "id": "9f2c7e1a-4b3d-4e5f-8a6b-2c1d3e4f5a6b",
      "name": "Chris Lane"
    },
    "tags": [
      {
        "id": "3c9d2e1f-7a4b-4c6d-8e2f-1a3b5c7d9e0f",
        "name": "Roof"
      }
    ],
    "download_url": "https://files.example.com/signed",
    "download_url_expires_at": "2026-09-28T15:06:12.000Z"
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 404 Not found · 429 Rate limited · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Malformed query or request payload.

Usually happens when:
The request sends malformed query/body data.
What to do:
Validate request fields against the contract examples before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Missing plan access, capability, IP access, or scopes for this request.

Usually happens when:
The key is valid but the company’s plan does not include this (reads need a paid plan, writes need Premium or above), API access is suspended, the key IP allowlist does not match, or the key lacks the required scope.
What to do:
Upgrade the plan for writes, adjust the key IP allowlist, or create a key with the required scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

404 Not found

The photo was not found.

Usually happens when:
The photo does not exist, was deleted, is restricted, or belongs to another company.
What to do:
List photos again to get current ids.
Error response
{
  "error": "We couldn't find that customer. Check its ID and try again."
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle request volume and avoid immediate fan-out retries from parallel workers.
Retry guidance:
Honor the `Retry-After` header and use exponential backoff for repeated limits.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this request.
What to do:
Check API availability, then retry with backoff.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while reading or serializing the resource.
What to do:
Log the contract version and response body, then retry once before escalating to Asteri support.
Error response
{
  "error": "We couldn't load customers. Try again."
}
POST
/api/public/v1/photos/{photoId}/tagsAdd company tags to a photo.

Required permissions

write:photos

Success:200

Usage

cURL
curl -X POST \
  'http://localhost:3000/api/public/v1/photos/00M6L4J2DT8C8492E03F818F27/tags' \
  -H "Authorization: Bearer ast_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{
  "tag_ids": [
    "3c9d2e1f-7a4b-4c6d-8e2f-1a3b5c7d9e0f"
  ]
}'
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.photosTags.add({ photoId: '00M6L4J2DT8C8492E03F818F27' }, {
  "tag_ids": [
    "3c9d2e1f-7a4b-4c6d-8e2f-1a3b5c7d9e0f"
  ]
});

Request body

Tags to add to a photo. Tags already on the photo stay.

PhotoTagsRequest
{
  "tag_ids": [
    "3c9d2e1f-7a4b-4c6d-8e2f-1a3b5c7d9e0f"
  ]
}
View request fields
PhotoTagsRequest fields
FieldTypeRequiredDescription
tag_idsuuid[]YesUp to 20 tag ids from GET /tags.

Response body

PhotoResponse
{
  "data": {
    "id": "0MG4Z8K1QX3F9A7C2D4E6B8A0C",
    "job_id": "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10",
    "job_title": "Roof repair",
    "customer_id": null,
    "customer_name": "Dana Whitfield",
    "media_type": "photo",
    "content_type": "image/jpeg",
    "original_name": "front-elevation.jpg",
    "size_bytes": 1843200,
    "caption": "Flashing lifted at the chimney",
    "stage": "before",
    "captured_at": "2026-09-28T14:05:00.000Z",
    "created_at": "2026-09-28T14:06:12.000Z",
    "coordinates": {
      "lat": 39.3719,
      "lng": -76.4632
    },
    "uploaded_by": {
      "id": "9f2c7e1a-4b3d-4e5f-8a6b-2c1d3e4f5a6b",
      "name": "Chris Lane"
    },
    "tags": [
      {
        "id": "3c9d2e1f-7a4b-4c6d-8e2f-1a3b5c7d9e0f",
        "name": "Roof"
      }
    ],
    "download_url": "https://files.example.com/signed",
    "download_url_expires_at": "2026-09-28T15:06:12.000Z"
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 429 Rate limited · 413 Request too large · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Invalid request payload.

Usually happens when:
The request body is missing required fields or includes invalid field values.
What to do:
Validate the payload against the generated request schema and fix any `issues` entries before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Creating and changing records needs the Premium plan or above.

Usually happens when:
The company plan includes reading through the API but not writing, or the key lacks the write scope.
What to do:
Upgrade to Premium in Subscription settings, or create a key with the needed write scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle mutation throughput and avoid blind retries from queues or worker pools.
Retry guidance:
Honor the `Retry-After` header and replay writes only after the relevant limit window resets.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

413 Request too large

The JSON request body exceeds this route’s byte limit.

Usually happens when:
The request body is larger than this write route accepts.
What to do:
Reduce the request body and try again.
Error response
{
  "error": "The request is too large. Send a smaller JSON body and try again."
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this write.
What to do:
Check API availability, then retry with the same idempotency key when applicable.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while validating or persisting the mutation.
What to do:
Capture the response payload and contract version, then retry once before escalating.
Error response
{
  "error": "We couldn't load customers. Try again."
}
GET
/api/public/v1/photos/{photoId}/commentsList the comments on a photo, oldest first, replies included (each points at the comment it answers).

Required permissions

read:photos

Success:200

Request parameters

limitQuery · integer · Optional
Maximum records per page, from 1 to 100 (default 25).
cursorQuery · string · Optional
Opaque next_cursor from the previous response.

Usage

cURL
curl -X GET \
  'http://localhost:3000/api/public/v1/photos/00M6L4J2DT8C8492E03F818F27/comments' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.photosComments.list({ photoId: '00M6L4J2DT8C8492E03F818F27' });

Response body

PhotoCommentListResponse
{
  "data": [
    {
      "id": "4e6a8c0e-2b4d-4f6a-9c1e-3b5d7f9a1c3e",
      "photo_id": "0MG4Z8K1QX3F9A7C2D4E6B8A0C",
      "body": "Flashing needs a second pass on the north side.",
      "parent_comment_id": null,
      "author_type": "team",
      "author": {
        "id": "9f2c7e1a-4b3d-4e5f-8a6b-2c1d3e4f5a6b",
        "name": "Chris Lane"
      },
      "author_name": "Chris Lane",
      "created_at": "2026-09-28T15:20:00.000Z",
      "updated_at": null
    }
  ],
  "pagination": {
    "count": 2,
    "limit": 25,
    "next_cursor": null
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 429 Rate limited · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Malformed query or request payload.

Usually happens when:
The request sends malformed query/body data.
What to do:
Validate request fields against the contract examples before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Missing plan access, capability, IP access, or scopes for this request.

Usually happens when:
The key is valid but the company’s plan does not include this (reads need a paid plan, writes need Premium or above), API access is suspended, the key IP allowlist does not match, or the key lacks the required scope.
What to do:
Upgrade the plan for writes, adjust the key IP allowlist, or create a key with the required scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle request volume and avoid immediate fan-out retries from parallel workers.
Retry guidance:
Honor the `Retry-After` header and use exponential backoff for repeated limits.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this request.
What to do:
Check API availability, then retry with backoff.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while reading or serializing the resource.
What to do:
Log the contract version and response body, then retry once before escalating to Asteri support.
Error response
{
  "error": "We couldn't load customers. Try again."
}
POST
/api/public/v1/photos/{photoId}/commentsComment on a photo, or reply to one of its top-level comments. Credited to the person who made the key; the photo uploader and earlier commenters are told as in the app.

Required permissions

write:photos

Success:201

Usage

cURL
curl -X POST \
  'http://localhost:3000/api/public/v1/photos/00M6L4J2DT8C8492E03F818F27/comments' \
  -H "Authorization: Bearer ast_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{
  "body": "Flashing needs a second pass on the north side."
}'
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.photosComments.create({ photoId: '00M6L4J2DT8C8492E03F818F27' }, {
  "body": "Flashing needs a second pass on the north side."
});

Request body

A comment, or a reply to a top-level comment on the same photo. Credited to the person who made the API key; mentions are not taken here.

PhotoCommentRequest
{
  "body": "Flashing needs a second pass on the north side."
}
View request fields
PhotoCommentRequest fields
FieldTypeRequiredDescription
bodystringYesThe comment text, up to 5000 characters.
parent_comment_iduuidNoOptional top-level comment on this photo to reply to.

Response body

PhotoCommentResponse
{
  "data": {
    "id": "4e6a8c0e-2b4d-4f6a-9c1e-3b5d7f9a1c3e",
    "photo_id": "0MG4Z8K1QX3F9A7C2D4E6B8A0C",
    "body": "Flashing needs a second pass on the north side.",
    "parent_comment_id": null,
    "author_type": "team",
    "author": {
      "id": "9f2c7e1a-4b3d-4e5f-8a6b-2c1d3e4f5a6b",
      "name": "Chris Lane"
    },
    "author_name": "Chris Lane",
    "created_at": "2026-09-28T15:20:00.000Z",
    "updated_at": null
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 429 Rate limited · 413 Request too large · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Invalid request payload.

Usually happens when:
The request body is missing required fields or includes invalid field values.
What to do:
Validate the payload against the generated request schema and fix any `issues` entries before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Creating and changing records needs the Premium plan or above.

Usually happens when:
The company plan includes reading through the API but not writing, or the key lacks the write scope.
What to do:
Upgrade to Premium in Subscription settings, or create a key with the needed write scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle mutation throughput and avoid blind retries from queues or worker pools.
Retry guidance:
Honor the `Retry-After` header and replay writes only after the relevant limit window resets.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

413 Request too large

The JSON request body exceeds this route’s byte limit.

Usually happens when:
The request body is larger than this write route accepts.
What to do:
Reduce the request body and try again.
Error response
{
  "error": "The request is too large. Send a smaller JSON body and try again."
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this write.
What to do:
Check API availability, then retry with the same idempotency key when applicable.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while validating or persisting the mutation.
What to do:
Capture the response payload and contract version, then retry once before escalating.
Error response
{
  "error": "We couldn't load customers. Try again."
}
GET
/api/public/v1/photos/{photoId}/markupRead a photo's newest markup and a link to the marked-up image.

Required permissions

read:photos

Success:200

Usage

cURL
curl -X GET \
  'http://localhost:3000/api/public/v1/photos/00M6L4J2DT8C8492E03F818F27/markup' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.photosMarkup.get({ photoId: '00M6L4J2DT8C8492E03F818F27' });

Response body

PhotoMarkupResponse
{
  "data": {
    "photo_id": "0MG4Z8K1QX3F9A7C2D4E6B8A0C",
    "revision": 3,
    "shapes": [
      {
        "id": "s1",
        "type": "arrow",
        "v": 2,
        "points": [
          [
            0.2,
            0.3
          ],
          [
            0.45,
            0.5
          ]
        ],
        "style": {
          "stroke": "#ef4444"
        }
      }
    ],
    "edits": null,
    "created_at": "2026-09-28T15:30:00.000Z",
    "created_by": {
      "id": "9f2c7e1a-4b3d-4e5f-8a6b-2c1d3e4f5a6b",
      "name": "Chris Lane"
    },
    "image_url": "https://files.example.com/signed-markup",
    "image_url_expires_at": "2026-09-28T16:30:00.000Z",
    "image_size_bytes": 412880
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 429 Rate limited · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Malformed query or request payload.

Usually happens when:
The request sends malformed query/body data.
What to do:
Validate request fields against the contract examples before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Missing plan access, capability, IP access, or scopes for this request.

Usually happens when:
The key is valid but the company’s plan does not include this (reads need a paid plan, writes need Premium or above), API access is suspended, the key IP allowlist does not match, or the key lacks the required scope.
What to do:
Upgrade the plan for writes, adjust the key IP allowlist, or create a key with the required scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle request volume and avoid immediate fan-out retries from parallel workers.
Retry guidance:
Honor the `Retry-After` header and use exponential backoff for repeated limits.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this request.
What to do:
Check API availability, then retry with backoff.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while reading or serializing the resource.
What to do:
Log the contract version and response body, then retry once before escalating to Asteri support.
Error response
{
  "error": "We couldn't load customers. Try again."
}
PUT
/api/public/v1/photos/{photoId}/markupSave new markup on a photo as its next revision, the same way the Asteri apps do. Credited to the person who made the key; the marked-up image is drawn in the background and photo.updated follows. Videos can't be marked up.

Required permissions

write:photos

Success:200

Request parameters

Idempotency-KeyHeader · string · Optional
Optional retry key; client_key in the body also works.

Usage

cURL
curl -X PUT \
  'http://localhost:3000/api/public/v1/photos/00M6L4J2DT8C8492E03F818F27/markup' \
  -H "Authorization: Bearer ast_live_your_key" \
  -H "Idempotency-Key: partner-request-001" \
  -H "Content-Type: application/json" \
  -d '{
  "shapes": [
    {
      "id": "s1",
      "type": "arrow",
      "v": 2,
      "points": [
        [
          0.2,
          0.3
        ],
        [
          0.45,
          0.5
        ]
      ],
      "style": {
        "stroke": "#ef4444"
      }
    }
  ]
}'
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.photosMarkup.save({ photoId: '00M6L4J2DT8C8492E03F818F27' }, {
  "shapes": [
    {
      "id": "s1",
      "type": "arrow",
      "v": 2,
      "points": [
        [
          0.2,
          0.3
        ],
        [
          0.45,
          0.5
        ]
      ],
      "style": {
        "stroke": "#ef4444"
      }
    }
  ]
}, { idempotencyKey: 'partner-request-001' });

Request body

New markup for a photo, saved as its next revision (older revisions are kept). The shapes are the same ones the Asteri apps draw. Send the Idempotency-Key header (or client_key) so a retry returns the first save instead of adding another revision.

PhotoMarkupRequest
{
  "shapes": [
    {
      "id": "s1",
      "type": "arrow",
      "v": 2,
      "points": [
        [
          0.2,
          0.3
        ],
        [
          0.45,
          0.5
        ]
      ],
      "style": {
        "stroke": "#ef4444"
      }
    }
  ]
}
View request fields
PhotoMarkupRequest fields
FieldTypeRequiredDescription
shapesMarkupShape[]YesEvery shape to show on the photo, in drawing order (up to 500). An empty list clears the markup.
editsobject | nullNoRotate (0, 90, 180 or 270), crop (x, y, w, h as shares of the photo) and brightness, contrast and saturation (-1 to 1) the shapes are drawn on. Leave it out to keep the current edits; null clears them.
client_keystringNoOptional idempotency key, 8 to 128 letters, digits, dots, dashes, colons or underscores. The Idempotency-Key header does the same.

Response body

PhotoMarkupSaveResponse
{
  "data": {
    "photo_id": "0MG4Z8K1QX3F9A7C2D4E6B8A0C",
    "revision": 3,
    "shapes": [
      {
        "id": "s1",
        "type": "arrow",
        "v": 2,
        "points": [
          [
            0.2,
            0.3
          ],
          [
            0.45,
            0.5
          ]
        ],
        "style": {
          "stroke": "#ef4444"
        }
      }
    ],
    "edits": null,
    "created_at": "2026-09-28T15:30:00.000Z",
    "created_by": {
      "id": "9f2c7e1a-4b3d-4e5f-8a6b-2c1d3e4f5a6b",
      "name": "Chris Lane"
    },
    "image_url": "https://files.example.com/signed-markup",
    "image_url_expires_at": "2026-09-28T16:30:00.000Z",
    "image_size_bytes": 412880
  },
  "replayed": false
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 429 Rate limited · 413 Request too large · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Invalid request payload.

Usually happens when:
The request body is missing required fields or includes invalid field values.
What to do:
Validate the payload against the generated request schema and fix any `issues` entries before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Creating and changing records needs the Premium plan or above.

Usually happens when:
The company plan includes reading through the API but not writing, or the key lacks the write scope.
What to do:
Upgrade to Premium in Subscription settings, or create a key with the needed write scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle mutation throughput and avoid blind retries from queues or worker pools.
Retry guidance:
Honor the `Retry-After` header and replay writes only after the relevant limit window resets.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

413 Request too large

The JSON request body exceeds this route’s byte limit.

Usually happens when:
The request body is larger than this write route accepts.
What to do:
Reduce the request body and try again.
Error response
{
  "error": "The request is too large. Send a smaller JSON body and try again."
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this write.
What to do:
Check API availability, then retry with the same idempotency key when applicable.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while validating or persisting the mutation.
What to do:
Capture the response payload and contract version, then retry once before escalating.
Error response
{
  "error": "We couldn't load customers. Try again."
}
POST
/api/public/v1/photos/importsImport a photo from a public https link into a job (answers 202; poll the import or listen for photo.created).

Required permissions

write:photos

Success:200202

Request parameters

Idempotency-KeyHeader · string · Optional
Optional retry key; idempotency_key in the body also works.

Usage

cURL
curl -X POST \
  'http://localhost:3000/api/public/v1/photos/imports' \
  -H "Authorization: Bearer ast_live_your_key" \
  -H "Idempotency-Key: partner-request-001" \
  -H "Content-Type: application/json" \
  -d '{
  "job_id": "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10",
  "url": "https://images.example.com/roof/0412.jpg",
  "caption": "North slope",
  "tag_ids": [
    "3c9d2e1f-7a4b-4c6d-8e2f-1a3b5c7d9e0f"
  ]
}'
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.photosImports.create({
  "job_id": "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10",
  "url": "https://images.example.com/roof/0412.jpg",
  "caption": "North slope",
  "tag_ids": [
    "3c9d2e1f-7a4b-4c6d-8e2f-1a3b5c7d9e0f"
  ]
}, { idempotencyKey: 'partner-request-001' });

Request body

Import a photo from a public https link (JPEG, PNG, WebP or HEIC, up to 25 MB). Send an Idempotency-Key header to make retries safe.

PhotoImportRequest
{
  "job_id": "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10",
  "url": "https://images.example.com/roof/0412.jpg",
  "caption": "North slope",
  "tag_ids": [
    "3c9d2e1f-7a4b-4c6d-8e2f-1a3b5c7d9e0f"
  ]
}
View request fields
PhotoImportRequest fields
FieldTypeRequiredDescription
job_iduuidYesThe job to add the photo to.
urlstringYesPublic https link to the photo.
captionstringNoOptional caption, up to 2000 characters.
tag_idsuuid[]NoOptional tags to add.
captured_atdatetimeNoOptional time the photo was taken.
idempotency_keystringNoOptional key; the header wins when both are sent.

Response body

PhotoImportResponse
{
  "data": {
    "id": "7e1d2c3b-4a5f-4e6d-8c7b-9a0f1e2d3c4b",
    "status": "done",
    "job_id": "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10",
    "source_url": "https://images.example.com/roof/0412.jpg",
    "photo_id": "0MG4Z8K1QX3F9A7C2D4E6B8A0C",
    "error": null,
    "created_at": "2026-09-28T14:06:00.000Z",
    "completed_at": "2026-09-28T14:06:04.000Z"
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 429 Rate limited · 413 Request too large · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Invalid request payload.

Usually happens when:
The request body is missing required fields or includes invalid field values.
What to do:
Validate the payload against the generated request schema and fix any `issues` entries before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Creating and changing records needs the Premium plan or above.

Usually happens when:
The company plan includes reading through the API but not writing, or the key lacks the write scope.
What to do:
Upgrade to Premium in Subscription settings, or create a key with the needed write scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle mutation throughput and avoid blind retries from queues or worker pools.
Retry guidance:
Honor the `Retry-After` header and replay writes only after the relevant limit window resets.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

413 Request too large

The JSON request body exceeds this route’s byte limit.

Usually happens when:
The request body is larger than this write route accepts.
What to do:
Reduce the request body and try again.
Error response
{
  "error": "The request is too large. Send a smaller JSON body and try again."
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this write.
What to do:
Check API availability, then retry with the same idempotency key when applicable.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while validating or persisting the mutation.
What to do:
Capture the response payload and contract version, then retry once before escalating.
Error response
{
  "error": "We couldn't load customers. Try again."
}
GET
/api/public/v1/photos/imports/{importId}Read an import: queued, downloading, done (with photo_id) or failed (with a plain error).

Required permissions

read:photos

Success:200

Usage

cURL
curl -X GET \
  'http://localhost:3000/api/public/v1/photos/imports/8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.photosImports.get({ importId: '8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3' });

Response body

PhotoImportResponse
{
  "data": {
    "id": "7e1d2c3b-4a5f-4e6d-8c7b-9a0f1e2d3c4b",
    "status": "done",
    "job_id": "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10",
    "source_url": "https://images.example.com/roof/0412.jpg",
    "photo_id": "0MG4Z8K1QX3F9A7C2D4E6B8A0C",
    "error": null,
    "created_at": "2026-09-28T14:06:00.000Z",
    "completed_at": "2026-09-28T14:06:04.000Z"
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 429 Rate limited · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Malformed query or request payload.

Usually happens when:
The request sends malformed query/body data.
What to do:
Validate request fields against the contract examples before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Missing plan access, capability, IP access, or scopes for this request.

Usually happens when:
The key is valid but the company’s plan does not include this (reads need a paid plan, writes need Premium or above), API access is suspended, the key IP allowlist does not match, or the key lacks the required scope.
What to do:
Upgrade the plan for writes, adjust the key IP allowlist, or create a key with the required scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle request volume and avoid immediate fan-out retries from parallel workers.
Retry guidance:
Honor the `Retry-After` header and use exponential backoff for repeated limits.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this request.
What to do:
Check API availability, then retry with backoff.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while reading or serializing the resource.
What to do:
Log the contract version and response body, then retry once before escalating to Asteri support.
Error response
{
  "error": "We couldn't load customers. Try again."
}
POST
/api/public/v1/photos/uploadsStart a direct upload and get a signed link to PUT the bytes to.

Required permissions

write:photos

Success:201

Usage

cURL
curl -X POST \
  'http://localhost:3000/api/public/v1/photos/uploads' \
  -H "Authorization: Bearer ast_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{
  "job_id": "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10",
  "file_name": "front-elevation.jpg",
  "content_type": "image/jpeg",
  "size_bytes": 1843200
}'
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.photosUploads.create({
  "job_id": "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10",
  "file_name": "front-elevation.jpg",
  "content_type": "image/jpeg",
  "size_bytes": 1843200
});

Request body

Start a direct upload: photos up to 25 MB, MP4 and MOV videos up to 500 MB.

PhotoUploadRequest
{
  "job_id": "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10",
  "file_name": "front-elevation.jpg",
  "content_type": "image/jpeg",
  "size_bytes": 1843200
}
View request fields
PhotoUploadRequest fields
FieldTypeRequiredDescription
job_iduuidYesThe job to add the photo to.
file_namestringYesFile name.
content_typestringYesimage/jpeg, image/png, image/webp, image/heic, image/heif, video/mp4 or video/quicktime.
size_bytesintegerYesExact size in bytes.
captionstringNoOptional caption.
captured_atdatetimeNoOptional time the photo was taken.

Response body

PhotoUploadResponse
{
  "data": {
    "upload_id": "0MG4Z8K1QX3F9A7C2D4E6B8A0C",
    "upload_url": "https://files.example.com/signed-put",
    "upload_url_expires_at": "2026-09-28T15:06:00.000Z",
    "method": "PUT",
    "headers": {
      "Content-Type": "image/jpeg",
      "Content-Length": "1843200"
    }
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 429 Rate limited · 413 Request too large · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Invalid request payload.

Usually happens when:
The request body is missing required fields or includes invalid field values.
What to do:
Validate the payload against the generated request schema and fix any `issues` entries before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Creating and changing records needs the Premium plan or above.

Usually happens when:
The company plan includes reading through the API but not writing, or the key lacks the write scope.
What to do:
Upgrade to Premium in Subscription settings, or create a key with the needed write scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle mutation throughput and avoid blind retries from queues or worker pools.
Retry guidance:
Honor the `Retry-After` header and replay writes only after the relevant limit window resets.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

413 Request too large

The JSON request body exceeds this route’s byte limit.

Usually happens when:
The request body is larger than this write route accepts.
What to do:
Reduce the request body and try again.
Error response
{
  "error": "The request is too large. Send a smaller JSON body and try again."
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this write.
What to do:
Check API availability, then retry with the same idempotency key when applicable.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while validating or persisting the mutation.
What to do:
Capture the response payload and contract version, then retry once before escalating.
Error response
{
  "error": "We couldn't load customers. Try again."
}
POST
/api/public/v1/photos/uploads/{uploadId}/completeFinish a direct upload once the bytes are stored (safe to repeat).

Required permissions

write:photos

Success:200

Usage

cURL
curl -X POST \
  'http://localhost:3000/api/public/v1/photos/uploads/00M6L4J2DT8C8492E03F818F27/complete' \
  -H "Authorization: Bearer ast_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{}'
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.photosUploads.complete({ uploadId: '00M6L4J2DT8C8492E03F818F27' }, {});

Request body

Finish an upload once the bytes are stored. Safe to repeat.

PhotoUploadCompleteRequest
{}
View request fields
PhotoUploadCompleteRequest fields
FieldTypeRequiredDescription
tag_idsuuid[]NoOptional tags to add.

Response body

PhotoResponse
{
  "data": {
    "id": "0MG4Z8K1QX3F9A7C2D4E6B8A0C",
    "job_id": "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10",
    "job_title": "Roof repair",
    "customer_id": null,
    "customer_name": "Dana Whitfield",
    "media_type": "photo",
    "content_type": "image/jpeg",
    "original_name": "front-elevation.jpg",
    "size_bytes": 1843200,
    "caption": "Flashing lifted at the chimney",
    "stage": "before",
    "captured_at": "2026-09-28T14:05:00.000Z",
    "created_at": "2026-09-28T14:06:12.000Z",
    "coordinates": {
      "lat": 39.3719,
      "lng": -76.4632
    },
    "uploaded_by": {
      "id": "9f2c7e1a-4b3d-4e5f-8a6b-2c1d3e4f5a6b",
      "name": "Chris Lane"
    },
    "tags": [
      {
        "id": "3c9d2e1f-7a4b-4c6d-8e2f-1a3b5c7d9e0f",
        "name": "Roof"
      }
    ],
    "download_url": "https://files.example.com/signed",
    "download_url_expires_at": "2026-09-28T15:06:12.000Z"
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 429 Rate limited · 413 Request too large · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Invalid request payload.

Usually happens when:
The request body is missing required fields or includes invalid field values.
What to do:
Validate the payload against the generated request schema and fix any `issues` entries before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Creating and changing records needs the Premium plan or above.

Usually happens when:
The company plan includes reading through the API but not writing, or the key lacks the write scope.
What to do:
Upgrade to Premium in Subscription settings, or create a key with the needed write scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle mutation throughput and avoid blind retries from queues or worker pools.
Retry guidance:
Honor the `Retry-After` header and replay writes only after the relevant limit window resets.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

413 Request too large

The JSON request body exceeds this route’s byte limit.

Usually happens when:
The request body is larger than this write route accepts.
What to do:
Reduce the request body and try again.
Error response
{
  "error": "The request is too large. Send a smaller JSON body and try again."
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this write.
What to do:
Check API availability, then retry with the same idempotency key when applicable.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while validating or persisting the mutation.
What to do:
Capture the response payload and contract version, then retry once before escalating.
Error response
{
  "error": "We couldn't load customers. Try again."
}
View photos schemas and field examples

Photo

A job photo or video. Restricted files and clock photos are never returned. `download_url` lasts an hour (a day in webhooks).

Photo fields
FieldTypeRequiredDescription
idstringYesPhoto identifier.
job_iduuidYesThe job the photo belongs to.
job_titlestring | nullYesJob title.
customer_iduuid | nullYesThe job customer.
customer_namestring | nullYesCustomer name.
media_type"photo" | "video"YesPhoto or video.
content_typestringYesFile type.
original_namestringYesFile name.
size_bytesintegerYesSize in bytes.
captionstring | nullYesCaption.
stage"before" | "during" | "after" | nullYesStage of the work the photo shows.
captured_atdatetime | nullYesWhen the photo was taken.
created_atdatetimeYesWhen the photo was added.
coordinatesobject | nullYesWhere the photo was taken, when known.
uploaded_byobject | nullYesWho added the photo.
tagsPhotoTagSummary[]YesCompany tags on the photo.
download_urlstring | nullYesSigned download link.
download_url_expires_atdatetime | nullYesWhen the download link stops working.
Example Photo
{
  "id": "0MG4Z8K1QX3F9A7C2D4E6B8A0C",
  "job_id": "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10",
  "job_title": "Roof repair",
  "customer_id": null,
  "customer_name": "Dana Whitfield",
  "media_type": "photo",
  "content_type": "image/jpeg",
  "original_name": "front-elevation.jpg",
  "size_bytes": 1843200,
  "caption": "Flashing lifted at the chimney",
  "stage": "before",
  "captured_at": "2026-09-28T14:05:00.000Z",
  "created_at": "2026-09-28T14:06:12.000Z",
  "coordinates": {
    "lat": 39.3719,
    "lng": -76.4632
  },
  "uploaded_by": {
    "id": "9f2c7e1a-4b3d-4e5f-8a6b-2c1d3e4f5a6b",
    "name": "Chris Lane"
  },
  "tags": [
    {
      "id": "3c9d2e1f-7a4b-4c6d-8e2f-1a3b5c7d9e0f",
      "name": "Roof"
    }
  ],
  "download_url": "https://files.example.com/signed",
  "download_url_expires_at": "2026-09-28T15:06:12.000Z"
}

PhotoListResponse

Photos, newest first.

PhotoListResponse fields
FieldTypeRequiredDescription
dataPhoto[]YesPhotos matching the filters.
paginationPaginationYesPage information.
Example PhotoListResponse
{
  "data": [
    {
      "id": "0MG4Z8K1QX3F9A7C2D4E6B8A0C",
      "job_id": "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10",
      "job_title": "Roof repair",
      "customer_id": null,
      "customer_name": "Dana Whitfield",
      "media_type": "photo",
      "content_type": "image/jpeg",
      "original_name": "front-elevation.jpg",
      "size_bytes": 1843200,
      "caption": "Flashing lifted at the chimney",
      "stage": "before",
      "captured_at": "2026-09-28T14:05:00.000Z",
      "created_at": "2026-09-28T14:06:12.000Z",
      "coordinates": {
        "lat": 39.3719,
        "lng": -76.4632
      },
      "uploaded_by": {
        "id": "9f2c7e1a-4b3d-4e5f-8a6b-2c1d3e4f5a6b",
        "name": "Chris Lane"
      },
      "tags": [
        {
          "id": "3c9d2e1f-7a4b-4c6d-8e2f-1a3b5c7d9e0f",
          "name": "Roof"
        }
      ],
      "download_url": "https://files.example.com/signed",
      "download_url_expires_at": "2026-09-28T15:06:12.000Z"
    }
  ],
  "pagination": {
    "count": 2,
    "limit": 25,
    "next_cursor": null
  }
}

PhotoImportRequest

Import a photo from a public https link (JPEG, PNG, WebP or HEIC, up to 25 MB). Send an Idempotency-Key header to make retries safe.

PhotoImportRequest fields
FieldTypeRequiredDescription
job_iduuidYesThe job to add the photo to.
urlstringYesPublic https link to the photo.
captionstringNoOptional caption, up to 2000 characters.
tag_idsuuid[]NoOptional tags to add.
captured_atdatetimeNoOptional time the photo was taken.
idempotency_keystringNoOptional key; the header wins when both are sent.
Example PhotoImportRequest
{
  "job_id": "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10",
  "url": "https://images.example.com/roof/0412.jpg",
  "caption": "North slope",
  "tag_ids": [
    "3c9d2e1f-7a4b-4c6d-8e2f-1a3b5c7d9e0f"
  ]
}

PhotoImport

A photo import and its outcome.

PhotoImport fields
FieldTypeRequiredDescription
iduuidYesImport identifier.
status"queued" | "downloading" | "done" | "failed"YesWhere the import is.
job_iduuidYesThe job.
source_urlstringYesThe link imported from.
photo_idstring | nullYesThe saved photo, once done.
errorstring | nullYesWhy the import failed, in plain words.
created_atdatetimeYesWhen the import was asked for.
completed_atdatetime | nullYesWhen it finished.
Example PhotoImport
{
  "id": "7e1d2c3b-4a5f-4e6d-8c7b-9a0f1e2d3c4b",
  "status": "done",
  "job_id": "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10",
  "source_url": "https://images.example.com/roof/0412.jpg",
  "photo_id": "0MG4Z8K1QX3F9A7C2D4E6B8A0C",
  "error": null,
  "created_at": "2026-09-28T14:06:00.000Z",
  "completed_at": "2026-09-28T14:06:04.000Z"
}

PhotoUploadRequest

Start a direct upload: photos up to 25 MB, MP4 and MOV videos up to 500 MB.

PhotoUploadRequest fields
FieldTypeRequiredDescription
job_iduuidYesThe job to add the photo to.
file_namestringYesFile name.
content_typestringYesimage/jpeg, image/png, image/webp, image/heic, image/heif, video/mp4 or video/quicktime.
size_bytesintegerYesExact size in bytes.
captionstringNoOptional caption.
captured_atdatetimeNoOptional time the photo was taken.
Example PhotoUploadRequest
{
  "job_id": "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10",
  "file_name": "front-elevation.jpg",
  "content_type": "image/jpeg",
  "size_bytes": 1843200
}

PhotoUpload

Where to PUT the bytes. Send the listed headers exactly, then POST .../complete.

PhotoUpload fields
FieldTypeRequiredDescription
upload_idstringYesUpload identifier (the photo id once finished).
upload_urlstringYesSigned link for the PUT, valid for an hour.
upload_url_expires_atdatetimeYesWhen the link stops working.
method"PUT"YesHTTP method for the upload.
headersobjectYesHeaders to send with the PUT.
Example PhotoUpload
{
  "upload_id": "0MG4Z8K1QX3F9A7C2D4E6B8A0C",
  "upload_url": "https://files.example.com/signed-put",
  "upload_url_expires_at": "2026-09-28T15:06:00.000Z",
  "method": "PUT",
  "headers": {
    "Content-Type": "image/jpeg",
    "Content-Length": "1843200"
  }
}

PhotoComment

A comment on a photo. Team comments name the person; customer comments from the project page carry the name they typed; comments from a partner company working on the job are marked collaborator.

PhotoComment fields
FieldTypeRequiredDescription
iduuidYesComment identifier.
photo_idstringYesThe photo.
bodystringYesThe comment text.
parent_comment_iduuid | nullYesThe comment this one answers; null for a top-level comment.
author_type"team" | "guest" | "collaborator"YesWho wrote it.
authorobject | nullYesThe team member, for team comments.
author_namestring | nullYesThe name shown with the comment.
created_atdatetimeYesWhen it was written.
updated_atdatetime | nullYesWhen it was last edited.
Example PhotoComment
{
  "id": "4e6a8c0e-2b4d-4f6a-9c1e-3b5d7f9a1c3e",
  "photo_id": "0MG4Z8K1QX3F9A7C2D4E6B8A0C",
  "body": "Flashing needs a second pass on the north side.",
  "parent_comment_id": null,
  "author_type": "team",
  "author": {
    "id": "9f2c7e1a-4b3d-4e5f-8a6b-2c1d3e4f5a6b",
    "name": "Chris Lane"
  },
  "author_name": "Chris Lane",
  "created_at": "2026-09-28T15:20:00.000Z",
  "updated_at": null
}

PhotoCommentListResponse

Comments on a photo, oldest first, replies included.

PhotoCommentListResponse fields
FieldTypeRequiredDescription
dataPhotoComment[]YesComments.
paginationPaginationYesPage information.
Example PhotoCommentListResponse
{
  "data": [
    {
      "id": "4e6a8c0e-2b4d-4f6a-9c1e-3b5d7f9a1c3e",
      "photo_id": "0MG4Z8K1QX3F9A7C2D4E6B8A0C",
      "body": "Flashing needs a second pass on the north side.",
      "parent_comment_id": null,
      "author_type": "team",
      "author": {
        "id": "9f2c7e1a-4b3d-4e5f-8a6b-2c1d3e4f5a6b",
        "name": "Chris Lane"
      },
      "author_name": "Chris Lane",
      "created_at": "2026-09-28T15:20:00.000Z",
      "updated_at": null
    }
  ],
  "pagination": {
    "count": 2,
    "limit": 25,
    "next_cursor": null
  }
}

PhotoCommentRequest

A comment, or a reply to a top-level comment on the same photo. Credited to the person who made the API key; mentions are not taken here.

PhotoCommentRequest fields
FieldTypeRequiredDescription
bodystringYesThe comment text, up to 5000 characters.
parent_comment_iduuidNoOptional top-level comment on this photo to reply to.
Example PhotoCommentRequest
{
  "body": "Flashing needs a second pass on the north side."
}

PhotoMarkup

The newest markup saved on a photo: arrows, shapes, text and measurements (points are shares of the photo width and height), the photo edits they were drawn on, and a link to the marked-up image once it is drawn. Revision 0 means the photo has no markup.

PhotoMarkup fields
FieldTypeRequiredDescription
photo_idstringYesThe photo.
revisionintegerYesMarkup revision; 0 when there is none.
shapesMarkupShape[]YesShapes as saved, in drawing order.
editsobject | nullYesRotate, crop and color edits; null for the photo as taken.
created_atdatetime | nullYesWhen this revision was saved.
created_byobject | nullYesWho saved it.
image_urlstring | nullYesThe marked-up image (an hour), once drawn.
image_url_expires_atdatetime | nullYesWhen the image link stops working.
image_size_bytesinteger | nullYesSize of the marked-up image.
Example PhotoMarkup
{
  "photo_id": "0MG4Z8K1QX3F9A7C2D4E6B8A0C",
  "revision": 3,
  "shapes": [
    {
      "id": "s1",
      "type": "arrow",
      "v": 2,
      "points": [
        [
          0.2,
          0.3
        ],
        [
          0.45,
          0.5
        ]
      ],
      "style": {
        "stroke": "#ef4444"
      }
    }
  ],
  "edits": null,
  "created_at": "2026-09-28T15:30:00.000Z",
  "created_by": {
    "id": "9f2c7e1a-4b3d-4e5f-8a6b-2c1d3e4f5a6b",
    "name": "Chris Lane"
  },
  "image_url": "https://files.example.com/signed-markup",
  "image_url_expires_at": "2026-09-28T16:30:00.000Z",
  "image_size_bytes": 412880
}

PhotoMarkupRequest

New markup for a photo, saved as its next revision (older revisions are kept). The shapes are the same ones the Asteri apps draw. Send the Idempotency-Key header (or client_key) so a retry returns the first save instead of adding another revision.

PhotoMarkupRequest fields
FieldTypeRequiredDescription
shapesMarkupShape[]YesEvery shape to show on the photo, in drawing order (up to 500). An empty list clears the markup.
editsobject | nullNoRotate (0, 90, 180 or 270), crop (x, y, w, h as shares of the photo) and brightness, contrast and saturation (-1 to 1) the shapes are drawn on. Leave it out to keep the current edits; null clears them.
client_keystringNoOptional idempotency key, 8 to 128 letters, digits, dots, dashes, colons or underscores. The Idempotency-Key header does the same.
Example PhotoMarkupRequest
{
  "shapes": [
    {
      "id": "s1",
      "type": "arrow",
      "v": 2,
      "points": [
        [
          0.2,
          0.3
        ],
        [
          0.45,
          0.5
        ]
      ],
      "style": {
        "stroke": "#ef4444"
      }
    }
  ]
}

MarkupShape

One shape drawn on a photo, as saved. Points are shares of the photo width and height (0 to 1) for version 2 shapes.

MarkupShape fields
FieldTypeRequiredDescription
idstringYesShape identifier.
typestringYesarrow, circle, rect, freehand, text, measurement, logo, polyline, callout, timestamp or sticker.
vintegerNoVersion marker: 2 means sizes are relative to the photo.
pointsnumber[][]YesThe shape points as [x, y] pairs.
styleobjectNoStroke color, stroke width and fill.
labelstringNoA label, when set.
valuestringNoText for text shapes, the readout for measurements, the number for callouts.
Example MarkupShape
{
  "id": "s1",
  "type": "arrow",
  "v": 2,
  "points": [
    [
      0.2,
      0.3
    ],
    [
      0.45,
      0.5
    ]
  ],
  "style": {
    "stroke": "#ef4444"
  }
}

Tags

The company photo tags.

/api/public/v1/tags
GET
/api/public/v1/tagsList photo tags by name; `search` narrows by name.

Required permissions

read:photos

Success:200

Request parameters

searchQuery · string · Optional
Filter tags by name.

Usage

cURL
curl -X GET \
  'http://localhost:3000/api/public/v1/tags' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.tags.list();

Response body

TagListResponse
{
  "data": [
    {
      "id": "3c9d2e1f-7a4b-4c6d-8e2f-1a3b5c7d9e0f",
      "name": "Roof",
      "color": "#6366f1"
    }
  ]
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 429 Rate limited · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Malformed query or request payload.

Usually happens when:
The request sends malformed query/body data.
What to do:
Validate request fields against the contract examples before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Missing plan access, capability, IP access, or scopes for this request.

Usually happens when:
The key is valid but the company’s plan does not include this (reads need a paid plan, writes need Premium or above), API access is suspended, the key IP allowlist does not match, or the key lacks the required scope.
What to do:
Upgrade the plan for writes, adjust the key IP allowlist, or create a key with the required scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle request volume and avoid immediate fan-out retries from parallel workers.
Retry guidance:
Honor the `Retry-After` header and use exponential backoff for repeated limits.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this request.
What to do:
Check API availability, then retry with backoff.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while reading or serializing the resource.
What to do:
Log the contract version and response body, then retry once before escalating to Asteri support.
Error response
{
  "error": "We couldn't load customers. Try again."
}
View tags schemas and field examples

Tag

A company photo tag.

Tag fields
FieldTypeRequiredDescription
iduuidYesTag identifier.
namestringYesTag name.
colorstring | nullYesDisplay color.
Example Tag
{
  "id": "3c9d2e1f-7a4b-4c6d-8e2f-1a3b5c7d9e0f",
  "name": "Roof",
  "color": "#6366f1"
}

TagListResponse

The company photo tags, by name.

TagListResponse fields
FieldTypeRequiredDescription
dataTag[]YesTags.
Example TagListResponse
{
  "data": [
    {
      "id": "3c9d2e1f-7a4b-4c6d-8e2f-1a3b5c7d9e0f",
      "name": "Roof",
      "color": "#6366f1"
    }
  ]
}

Checklists

Job checklists with progress, steps and answers; add one to a job from a template.

/api/public/v1/checklists
GET
/api/public/v1/checklistsList checklists newest first; filter by job_id or status.

Required permissions

read:checklists

Success:200

Request parameters

limitQuery · integer · Optional
Maximum records per page, from 1 to 100 (default 25).
cursorQuery · string · Optional
Opaque next_cursor from the previous response.
job_idQuery · uuid · Optional
Filter by job ID.
statusQuery · string · Optional
Filter by checklist status.

Usage

cURL
curl -X GET \
  'http://localhost:3000/api/public/v1/checklists' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.checklists.list();

Response body

ChecklistListResponse
{
  "data": [
    {
      "id": "6b8d0f2a-4c6e-4a8b-9d1f-3a5c7e9b1d2f",
      "job_id": "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10",
      "job_visit_id": null,
      "name": "Roof inspection",
      "status": "completed",
      "total_steps": 8,
      "completed_steps": 8,
      "created_at": "2026-09-28T13:00:00.000Z"
    }
  ],
  "pagination": {
    "count": 2,
    "limit": 25,
    "next_cursor": null
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 429 Rate limited · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Malformed query or request payload.

Usually happens when:
The request sends malformed query/body data.
What to do:
Validate request fields against the contract examples before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Missing plan access, capability, IP access, or scopes for this request.

Usually happens when:
The key is valid but the company’s plan does not include this (reads need a paid plan, writes need Premium or above), API access is suspended, the key IP allowlist does not match, or the key lacks the required scope.
What to do:
Upgrade the plan for writes, adjust the key IP allowlist, or create a key with the required scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle request volume and avoid immediate fan-out retries from parallel workers.
Retry guidance:
Honor the `Retry-After` header and use exponential backoff for repeated limits.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this request.
What to do:
Check API availability, then retry with backoff.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while reading or serializing the resource.
What to do:
Log the contract version and response body, then retry once before escalating to Asteri support.
Error response
{
  "error": "We couldn't load customers. Try again."
}
GET
/api/public/v1/checklists/{checklistId}Read one checklist with its steps.

Required permissions

read:checklists

Success:200

Usage

cURL
curl -X GET \
  'http://localhost:3000/api/public/v1/checklists/8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.checklists.get({ checklistId: '8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3' });

Response body

ChecklistResponse
{
  "data": {
    "id": "6b8d0f2a-4c6e-4a8b-9d1f-3a5c7e9b1d2f",
    "job_id": "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10",
    "job_visit_id": null,
    "name": "Roof inspection",
    "status": "completed",
    "total_steps": 8,
    "completed_steps": 8,
    "created_at": "2026-09-28T13:00:00.000Z",
    "steps": [
      {
        "id": "2d4f6a8c-1e3b-4d5f-9a7c-0e2f4a6c8e1b",
        "title": "Photo of the flashing",
        "section": "Roof",
        "type": "task",
        "required": true,
        "completed": true,
        "not_applicable": false,
        "completed_at": "2026-09-28T15:10:00.000Z",
        "answer": null,
        "notes": null,
        "photo_count": 2
      }
    ]
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 429 Rate limited · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Malformed query or request payload.

Usually happens when:
The request sends malformed query/body data.
What to do:
Validate request fields against the contract examples before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Missing plan access, capability, IP access, or scopes for this request.

Usually happens when:
The key is valid but the company’s plan does not include this (reads need a paid plan, writes need Premium or above), API access is suspended, the key IP allowlist does not match, or the key lacks the required scope.
What to do:
Upgrade the plan for writes, adjust the key IP allowlist, or create a key with the required scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle request volume and avoid immediate fan-out retries from parallel workers.
Retry guidance:
Honor the `Retry-After` header and use exponential backoff for repeated limits.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this request.
What to do:
Check API availability, then retry with backoff.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while reading or serializing the resource.
What to do:
Log the contract version and response body, then retry once before escalating to Asteri support.
Error response
{
  "error": "We couldn't load customers. Try again."
}
POST
/api/public/v1/checklistsAdd a checklist to a job (or one of its visits) from a template. The same template twice returns the checklist it already made.

Required permissions

write:checklists

Success:200201

Usage

cURL
curl -X POST \
  'http://localhost:3000/api/public/v1/checklists' \
  -H "Authorization: Bearer ast_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{
  "job_id": "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10",
  "template_id": "8a1c3e5f-7b9d-4f1a-8c3e-5a7c9e1b3d5f"
}'
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.checklists.create({
  "job_id": "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10",
  "template_id": "8a1c3e5f-7b9d-4f1a-8c3e-5a7c9e1b3d5f"
});

Request body

Add a checklist to a job from a template. Premium plan and above.

ChecklistCreateRequest
{
  "job_id": "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10",
  "template_id": "8a1c3e5f-7b9d-4f1a-8c3e-5a7c9e1b3d5f"
}
View request fields
ChecklistCreateRequest fields
FieldTypeRequiredDescription
job_iduuidYesThe job.
template_iduuidYesA template from GET /checklist-templates.
job_visit_iduuidNoOptional visit of the job the checklist belongs to.

Response body

ChecklistCreateResponse
{
  "data": {
    "id": "6b8d0f2a-4c6e-4a8b-9d1f-3a5c7e9b1d2f",
    "job_id": "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10",
    "job_visit_id": null,
    "name": "Roof inspection",
    "status": "completed",
    "total_steps": 8,
    "completed_steps": 8,
    "created_at": "2026-09-28T13:00:00.000Z",
    "steps": [
      {
        "id": "2d4f6a8c-1e3b-4d5f-9a7c-0e2f4a6c8e1b",
        "title": "Photo of the flashing",
        "section": "Roof",
        "type": "task",
        "required": true,
        "completed": true,
        "not_applicable": false,
        "completed_at": "2026-09-28T15:10:00.000Z",
        "answer": null,
        "notes": null,
        "photo_count": 2
      }
    ]
  },
  "created": true
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 429 Rate limited · 413 Request too large · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Invalid request payload.

Usually happens when:
The request body is missing required fields or includes invalid field values.
What to do:
Validate the payload against the generated request schema and fix any `issues` entries before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Creating and changing records needs the Premium plan or above.

Usually happens when:
The company plan includes reading through the API but not writing, or the key lacks the write scope.
What to do:
Upgrade to Premium in Subscription settings, or create a key with the needed write scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle mutation throughput and avoid blind retries from queues or worker pools.
Retry guidance:
Honor the `Retry-After` header and replay writes only after the relevant limit window resets.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

413 Request too large

The JSON request body exceeds this route’s byte limit.

Usually happens when:
The request body is larger than this write route accepts.
What to do:
Reduce the request body and try again.
Error response
{
  "error": "The request is too large. Send a smaller JSON body and try again."
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this write.
What to do:
Check API availability, then retry with the same idempotency key when applicable.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while validating or persisting the mutation.
What to do:
Capture the response payload and contract version, then retry once before escalating.
Error response
{
  "error": "We couldn't load customers. Try again."
}
View checklists schemas and field examples

Checklist

A job checklist with progress. `steps` is included when reading one checklist.

Checklist fields
FieldTypeRequiredDescription
iduuidYesChecklist identifier.
job_iduuidYesThe job.
job_visit_iduuid | nullYesThe visit, when the checklist belongs to one.
namestringYesChecklist name.
status"not_started" | "in_progress" | "completed" | "skipped"YesProgress, from the steps.
total_stepsintegerYesVisible steps.
completed_stepsintegerYesSteps done.
created_atdatetime | nullYesWhen the checklist was added.
stepsChecklistStep[]NoSteps, in order (single reads only).
Example Checklist
{
  "id": "6b8d0f2a-4c6e-4a8b-9d1f-3a5c7e9b1d2f",
  "job_id": "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10",
  "job_visit_id": null,
  "name": "Roof inspection",
  "status": "completed",
  "total_steps": 8,
  "completed_steps": 8,
  "created_at": "2026-09-28T13:00:00.000Z",
  "steps": [
    {
      "id": "2d4f6a8c-1e3b-4d5f-9a7c-0e2f4a6c8e1b",
      "title": "Photo of the flashing",
      "section": "Roof",
      "type": "task",
      "required": true,
      "completed": true,
      "not_applicable": false,
      "completed_at": "2026-09-28T15:10:00.000Z",
      "answer": null,
      "notes": null,
      "photo_count": 2
    }
  ]
}

ChecklistStep

One visible step of a job checklist.

ChecklistStep fields
FieldTypeRequiredDescription
iduuidYesStep identifier.
titlestringYesWhat the step asks.
sectionstring | nullYesSection name.
typestringYesStep type (task, question, photo, and so on).
requiredbooleanYesWhether the step is required.
completedbooleanYesWhether the step is done.
not_applicablebooleanYesMarked not applicable (counts as done).
completed_atdatetime | nullYesWhen it was done.
answeranyYesThe answer, for question steps.
notesstring | nullYesNotes.
photo_countintegerYesPhotos attached to the step.
Example ChecklistStep
{
  "id": "2d4f6a8c-1e3b-4d5f-9a7c-0e2f4a6c8e1b",
  "title": "Photo of the flashing",
  "section": "Roof",
  "type": "task",
  "required": true,
  "completed": true,
  "not_applicable": false,
  "completed_at": "2026-09-28T15:10:00.000Z",
  "answer": null,
  "notes": null,
  "photo_count": 2
}

ChecklistListResponse

Checklists, newest first.

ChecklistListResponse fields
FieldTypeRequiredDescription
dataChecklist[]YesChecklists.
paginationPaginationYesPage information.
Example ChecklistListResponse
{
  "data": [
    {
      "id": "6b8d0f2a-4c6e-4a8b-9d1f-3a5c7e9b1d2f",
      "job_id": "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10",
      "job_visit_id": null,
      "name": "Roof inspection",
      "status": "completed",
      "total_steps": 8,
      "completed_steps": 8,
      "created_at": "2026-09-28T13:00:00.000Z"
    }
  ],
  "pagination": {
    "count": 2,
    "limit": 25,
    "next_cursor": null
  }
}

ChecklistCreateRequest

Add a checklist to a job from a template. Premium plan and above.

ChecklistCreateRequest fields
FieldTypeRequiredDescription
job_iduuidYesThe job.
template_iduuidYesA template from GET /checklist-templates.
job_visit_iduuidNoOptional visit of the job the checklist belongs to.
Example ChecklistCreateRequest
{
  "job_id": "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10",
  "template_id": "8a1c3e5f-7b9d-4f1a-8c3e-5a7c9e1b3d5f"
}

ChecklistCreateResponse

The checklist with its steps. Answers 201 when it was added; 200 with `created: false` when the job (or visit) already had this template.

ChecklistCreateResponse fields
FieldTypeRequiredDescription
dataChecklistYesThe checklist.
createdbooleanYesWhether this request added it.
Example ChecklistCreateResponse
{
  "data": {
    "id": "6b8d0f2a-4c6e-4a8b-9d1f-3a5c7e9b1d2f",
    "job_id": "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10",
    "job_visit_id": null,
    "name": "Roof inspection",
    "status": "completed",
    "total_steps": 8,
    "completed_steps": 8,
    "created_at": "2026-09-28T13:00:00.000Z",
    "steps": [
      {
        "id": "2d4f6a8c-1e3b-4d5f-9a7c-0e2f4a6c8e1b",
        "title": "Photo of the flashing",
        "section": "Roof",
        "type": "task",
        "required": true,
        "completed": true,
        "not_applicable": false,
        "completed_at": "2026-09-28T15:10:00.000Z",
        "answer": null,
        "notes": null,
        "photo_count": 2
      }
    ]
  },
  "created": true
}

Checklist templates

The company checklist templates, for adding checklists to jobs.

/api/public/v1/checklist-templates
GET
/api/public/v1/checklist-templatesList the checklist templates that are turned on, by name.

Required permissions

read:checklists

Success:200

Usage

cURL
curl -X GET \
  'http://localhost:3000/api/public/v1/checklist-templates' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.checklistTemplates.list();

Response body

ChecklistTemplateListResponse
{
  "data": [
    {
      "id": "8a1c3e5f-7b9d-4f1a-8c3e-5a7c9e1b3d5f",
      "name": "Roof inspection",
      "description": null,
      "step_count": 12,
      "service_id": null
    }
  ]
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 429 Rate limited · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Malformed query or request payload.

Usually happens when:
The request sends malformed query/body data.
What to do:
Validate request fields against the contract examples before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Missing plan access, capability, IP access, or scopes for this request.

Usually happens when:
The key is valid but the company’s plan does not include this (reads need a paid plan, writes need Premium or above), API access is suspended, the key IP allowlist does not match, or the key lacks the required scope.
What to do:
Upgrade the plan for writes, adjust the key IP allowlist, or create a key with the required scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle request volume and avoid immediate fan-out retries from parallel workers.
Retry guidance:
Honor the `Retry-After` header and use exponential backoff for repeated limits.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this request.
What to do:
Check API availability, then retry with backoff.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while reading or serializing the resource.
What to do:
Log the contract version and response body, then retry once before escalating to Asteri support.
Error response
{
  "error": "We couldn't load customers. Try again."
}
View checklist templates schemas and field examples

ChecklistTemplate

A saved checklist template that is turned on.

ChecklistTemplate fields
FieldTypeRequiredDescription
iduuidYesTemplate identifier.
namestringYesTemplate name.
descriptionstring | nullYesDescription.
step_countintegerYesSteps in the template.
service_iduuid | nullYesThe service the template is for, when set.
Example ChecklistTemplate
{
  "id": "8a1c3e5f-7b9d-4f1a-8c3e-5a7c9e1b3d5f",
  "name": "Roof inspection",
  "description": null,
  "step_count": 12,
  "service_id": null
}

ChecklistTemplateListResponse

The company checklist templates that are turned on, by name.

ChecklistTemplateListResponse fields
FieldTypeRequiredDescription
dataChecklistTemplate[]YesTemplates.
Example ChecklistTemplateListResponse
{
  "data": [
    {
      "id": "8a1c3e5f-7b9d-4f1a-8c3e-5a7c9e1b3d5f",
      "name": "Roof inspection",
      "description": null,
      "step_count": 12,
      "service_id": null
    }
  ]
}

Photo reports

Photo reports on jobs, with their sections and photos.

/api/public/v1/reports
GET
/api/public/v1/reportsList photo reports newest first; filter by job_id or status.

Required permissions

read:reports

Success:200

Request parameters

limitQuery · integer · Optional
Maximum records per page, from 1 to 100 (default 25).
cursorQuery · string · Optional
Opaque next_cursor from the previous response.
job_idQuery · uuid · Optional
Filter by job ID.
statusQuery · string · Optional
Filter by report status.

Usage

cURL
curl -X GET \
  'http://localhost:3000/api/public/v1/reports' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.reports.list();

Response body

ReportListResponse
{
  "data": [
    {
      "id": "8c0e2a4b-6d8f-4b0d-a2c4-5e7a9c1e3f5a",
      "name": "Roof replacement",
      "job_id": "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10",
      "status": "completed",
      "photo_count": 12,
      "completed_at": "2026-09-28T16:00:00.000Z",
      "created_at": "2026-09-28T13:00:00.000Z",
      "sections": [
        {
          "title": "Before",
          "stage": "before",
          "required_photos": 4,
          "photo_ids": [
            "0MG4Z8K1QX3F9A7C2D4E6B8A0C"
          ],
          "notes": null
        }
      ]
    }
  ],
  "pagination": {
    "count": 2,
    "limit": 25,
    "next_cursor": null
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 429 Rate limited · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Malformed query or request payload.

Usually happens when:
The request sends malformed query/body data.
What to do:
Validate request fields against the contract examples before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Missing plan access, capability, IP access, or scopes for this request.

Usually happens when:
The key is valid but the company’s plan does not include this (reads need a paid plan, writes need Premium or above), API access is suspended, the key IP allowlist does not match, or the key lacks the required scope.
What to do:
Upgrade the plan for writes, adjust the key IP allowlist, or create a key with the required scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle request volume and avoid immediate fan-out retries from parallel workers.
Retry guidance:
Honor the `Retry-After` header and use exponential backoff for repeated limits.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this request.
What to do:
Check API availability, then retry with backoff.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while reading or serializing the resource.
What to do:
Log the contract version and response body, then retry once before escalating to Asteri support.
Error response
{
  "error": "We couldn't load customers. Try again."
}
View photo reports schemas and field examples

Report

A photo report on a job.

Report fields
FieldTypeRequiredDescription
iduuidYesReport identifier.
namestring | nullYesReport template name.
job_iduuid | nullYesThe job.
status"in_progress" | "completed" | "abandoned"YesReport status.
photo_countintegerYesPhotos in the report.
completed_atdatetime | nullYesWhen it was completed.
created_atdatetime | nullYesWhen it was started.
sectionsReportSection[]YesSections, in order.
Example Report
{
  "id": "8c0e2a4b-6d8f-4b0d-a2c4-5e7a9c1e3f5a",
  "name": "Roof replacement",
  "job_id": "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10",
  "status": "completed",
  "photo_count": 12,
  "completed_at": "2026-09-28T16:00:00.000Z",
  "created_at": "2026-09-28T13:00:00.000Z",
  "sections": [
    {
      "title": "Before",
      "stage": "before",
      "required_photos": 4,
      "photo_ids": [
        "0MG4Z8K1QX3F9A7C2D4E6B8A0C"
      ],
      "notes": null
    }
  ]
}

ReportSection

One section of a photo report.

ReportSection fields
FieldTypeRequiredDescription
titlestringYesSection title.
stage"before" | "during" | "after" | nullYesStage.
required_photosintegerYesPhotos the section asks for.
photo_idsstring[]YesPhotos in the section (never restricted ones).
notesstring | nullYesSection notes.
Example ReportSection
{
  "title": "Before",
  "stage": "before",
  "required_photos": 4,
  "photo_ids": [
    "0MG4Z8K1QX3F9A7C2D4E6B8A0C"
  ],
  "notes": null
}

ReportListResponse

Photo reports, newest first.

ReportListResponse fields
FieldTypeRequiredDescription
dataReport[]YesReports.
paginationPaginationYesPage information.
Example ReportListResponse
{
  "data": [
    {
      "id": "8c0e2a4b-6d8f-4b0d-a2c4-5e7a9c1e3f5a",
      "name": "Roof replacement",
      "job_id": "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10",
      "status": "completed",
      "photo_count": 12,
      "completed_at": "2026-09-28T16:00:00.000Z",
      "created_at": "2026-09-28T13:00:00.000Z",
      "sections": [
        {
          "title": "Before",
          "stage": "before",
          "required_photos": 4,
          "photo_ids": [
            "0MG4Z8K1QX3F9A7C2D4E6B8A0C"
          ],
          "notes": null
        }
      ]
    }
  ],
  "pagination": {
    "count": 2,
    "limit": 25,
    "next_cursor": null
  }
}

Documents

Saved documents: reports, daily logs, summaries and packets built from job photos and job data.

/api/public/v1/documents
GET
/api/public/v1/documentsList documents, newest change first; filter by job_id, customer_id, kind, status or archived=true.

Required permissions

read:documents

Success:200

Request parameters

limitQuery · integer · Optional
Maximum records per page, from 1 to 100 (default 25).
cursorQuery · string · Optional
Opaque next_cursor from the previous response.
job_idQuery · uuid · Optional
Filter by job ID.
customer_idQuery · uuid · Optional
Filter by customer ID.
kindQuery · string · Optional
Filter by document kind.
statusQuery · draft | final · Optional
Filter by document status.
archivedQuery · boolean · Optional
Include archived documents when true.

Usage

cURL
curl -X GET \
  'http://localhost:3000/api/public/v1/documents' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.documents.list();

Response body

DocumentListResponse
{
  "data": [
    {
      "id": "2d4f6a8c-0e1b-4d3f-9a5c-7e9b1d3f5a7c",
      "title": "Roof inspection",
      "kind": "report",
      "status": "draft",
      "archived": false,
      "revision": 3,
      "job_ids": [
        "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10"
      ],
      "customer_id": null,
      "template_id": null,
      "shown_to_customer": false,
      "created_by": "9f2c7e1a-4b3d-4e5f-8a6b-2c1d3e4f5a6b",
      "created_at": "2026-09-28T13:00:00.000Z",
      "updated_at": "2026-09-28T16:00:00.000Z",
      "finalized_at": null
    }
  ],
  "pagination": {
    "count": 2,
    "limit": 25,
    "next_cursor": null
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 429 Rate limited · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Malformed query or request payload.

Usually happens when:
The request sends malformed query/body data.
What to do:
Validate request fields against the contract examples before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Missing plan access, capability, IP access, or scopes for this request.

Usually happens when:
The key is valid but the company’s plan does not include this (reads need a paid plan, writes need Premium or above), API access is suspended, the key IP allowlist does not match, or the key lacks the required scope.
What to do:
Upgrade the plan for writes, adjust the key IP allowlist, or create a key with the required scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle request volume and avoid immediate fan-out retries from parallel workers.
Retry guidance:
Honor the `Retry-After` header and use exponential backoff for repeated limits.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this request.
What to do:
Check API availability, then retry with backoff.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while reading or serializing the resource.
What to do:
Log the contract version and response body, then retry once before escalating to Asteri support.
Error response
{
  "error": "We couldn't load customers. Try again."
}
GET
/api/public/v1/documents/{documentId}Read one document with its blocks and its words.

Required permissions

read:documents

Success:200

Usage

cURL
curl -X GET \
  'http://localhost:3000/api/public/v1/documents/8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.documents.get({ documentId: '8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3' });

Response body

DocumentResponse
{
  "data": {
    "id": "2d4f6a8c-0e1b-4d3f-9a5c-7e9b1d3f5a7c",
    "title": "Roof inspection",
    "kind": "report",
    "status": "draft",
    "archived": false,
    "revision": 3,
    "job_ids": [
      "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10"
    ],
    "customer_id": null,
    "template_id": null,
    "shown_to_customer": false,
    "created_by": "9f2c7e1a-4b3d-4e5f-8a6b-2c1d3e4f5a6b",
    "created_at": "2026-09-28T13:00:00.000Z",
    "updated_at": "2026-09-28T16:00:00.000Z",
    "finalized_at": null,
    "content": {
      "version": 1,
      "frozen": null,
      "page": {
        "size": "letter",
        "orientation": "portrait",
        "margins": "normal"
      },
      "theme": {
        "accentColor": "#1F2937",
        "font": "sans"
      },
      "header": {
        "enabled": false,
        "left": "",
        "center": "",
        "right": "",
        "showLogo": true
      },
      "footer": {
        "enabled": false,
        "text": "",
        "showPageNumbers": true
      },
      "blocks": [
        {
          "id": "b1",
          "type": "heading",
          "level": 1,
          "text": "Roof inspection"
        },
        {
          "id": "b2",
          "type": "rich_text",
          "html": "<p>Two cracked tiles over the garage.</p>"
        }
      ]
    },
    "photo_count": 0,
    "text": "# Roof inspection\n\nTwo cracked tiles over the garage.",
    "text_cut_short": false
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 404 Not found · 429 Rate limited · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Malformed query or request payload.

Usually happens when:
The request sends malformed query/body data.
What to do:
Validate request fields against the contract examples before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Missing plan access, capability, IP access, or scopes for this request.

Usually happens when:
The key is valid but the company’s plan does not include this (reads need a paid plan, writes need Premium or above), API access is suspended, the key IP allowlist does not match, or the key lacks the required scope.
What to do:
Upgrade the plan for writes, adjust the key IP allowlist, or create a key with the required scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

404 Not found

No such document for this key.

Usually happens when:
The document does not exist, is in the trash, is restricted, or belongs to another company.
What to do:
List documents again and use an id from the list.
Error response
{
  "error": "We couldn't find that customer. Check its ID and try again."
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle request volume and avoid immediate fan-out retries from parallel workers.
Retry guidance:
Honor the `Retry-After` header and use exponential backoff for repeated limits.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this request.
What to do:
Check API availability, then retry with backoff.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while reading or serializing the resource.
What to do:
Log the contract version and response body, then retry once before escalating to Asteri support.
Error response
{
  "error": "We couldn't load customers. Try again."
}
POST
/api/public/v1/documentsMake a draft document on one job, or a packet over several jobs of one customer.

Required permissions

write:documents

Success:201

Usage

cURL
curl -X POST \
  'http://localhost:3000/api/public/v1/documents' \
  -H "Authorization: Bearer ast_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{
  "kind": "report",
  "jobs": [
    {
      "job_id": "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10"
    }
  ]
}'
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.documents.create({
  "kind": "report",
  "jobs": [
    {
      "job_id": "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10"
    }
  ]
});

Request body

Make a draft on one job, or a packet over several jobs of one customer. Premium plan and above.

DocumentCreateRequest
{
  "kind": "report",
  "jobs": [
    {
      "job_id": "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10"
    }
  ]
}
View request fields
DocumentCreateRequest fields
FieldTypeRequiredDescription
kind"report" | "daily_log" | "summary" | "custom" | "warranty_packet" | "insurance_packet"YesWhat kind of document to make.
jobsDocumentJobSelection[]YesThe jobs, all of one customer; the first is the job it is made from.
titlestringNoOptional title; defaults to the kind and the job or customer.
startDocumentStartNoOptional; the layout for the kind by default.

Response body

DocumentResponse
{
  "data": {
    "id": "2d4f6a8c-0e1b-4d3f-9a5c-7e9b1d3f5a7c",
    "title": "Roof inspection",
    "kind": "report",
    "status": "draft",
    "archived": false,
    "revision": 3,
    "job_ids": [
      "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10"
    ],
    "customer_id": null,
    "template_id": null,
    "shown_to_customer": false,
    "created_by": "9f2c7e1a-4b3d-4e5f-8a6b-2c1d3e4f5a6b",
    "created_at": "2026-09-28T13:00:00.000Z",
    "updated_at": "2026-09-28T16:00:00.000Z",
    "finalized_at": null,
    "content": {
      "version": 1,
      "frozen": null,
      "page": {
        "size": "letter",
        "orientation": "portrait",
        "margins": "normal"
      },
      "theme": {
        "accentColor": "#1F2937",
        "font": "sans"
      },
      "header": {
        "enabled": false,
        "left": "",
        "center": "",
        "right": "",
        "showLogo": true
      },
      "footer": {
        "enabled": false,
        "text": "",
        "showPageNumbers": true
      },
      "blocks": [
        {
          "id": "b1",
          "type": "heading",
          "level": 1,
          "text": "Roof inspection"
        },
        {
          "id": "b2",
          "type": "rich_text",
          "html": "<p>Two cracked tiles over the garage.</p>"
        }
      ]
    },
    "photo_count": 0,
    "text": "# Roof inspection\n\nTwo cracked tiles over the garage.",
    "text_cut_short": false
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 404 Not found · 412 Precondition failed · 429 Rate limited · 413 Request too large · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Invalid request payload.

Usually happens when:
The request body is missing required fields or includes invalid field values.
What to do:
Validate the payload against the generated request schema and fix any `issues` entries before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Creating and changing records needs the Premium plan or above.

Usually happens when:
The company plan includes reading through the API but not writing, or the key lacks the write scope.
What to do:
Upgrade to Premium in Subscription settings, or create a key with the needed write scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

404 Not found

A job or the template was not found.

Usually happens when:
A job id or template id is not one of this company’s.
What to do:
Check the ids against GET /appointments and the company’s document templates.
Error response
{
  "error": "We couldn't find that customer. Check its ID and try again."
}

412 Precondition failed

A job is in the trash or was merged into another job.

Usually happens when:
One of the jobs was moved to the trash or merged.
What to do:
Restore the job in Asteri, or leave it out.
Error response
{
  "error": "Configure or manually resolve sales tax before publishing."
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle mutation throughput and avoid blind retries from queues or worker pools.
Retry guidance:
Honor the `Retry-After` header and replay writes only after the relevant limit window resets.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

413 Request too large

The JSON request body exceeds this route’s byte limit.

Usually happens when:
The request body is larger than this write route accepts.
What to do:
Reduce the request body and try again.
Error response
{
  "error": "The request is too large. Send a smaller JSON body and try again."
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this write.
What to do:
Check API availability, then retry with the same idempotency key when applicable.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while validating or persisting the mutation.
What to do:
Capture the response payload and contract version, then retry once before escalating.
Error response
{
  "error": "We couldn't load customers. Try again."
}
PATCH
/api/public/v1/documents/{documentId}Edit a draft’s title or content at the revision you last read.

Required permissions

write:documents

Success:200

Usage

cURL
curl -X PATCH \
  'http://localhost:3000/api/public/v1/documents/8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3' \
  -H "Authorization: Bearer ast_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{
  "expected_revision": 3,
  "title": "Roof inspection, garage"
}'
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.documents.update({ documentId: '8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3' }, {
  "expected_revision": 3,
  "title": "Roof inspection, garage"
});

Request body

Edit a draft: a new title, new content (the whole block list, as GET returns it) or both. Premium plan and above.

DocumentUpdateRequest
{
  "expected_revision": 3,
  "title": "Roof inspection, garage"
}
View request fields
DocumentUpdateRequest fields
FieldTypeRequiredDescription
expected_revisionintegerYesThe revision you last read.
titlestringNoOptional new title.
contentDocumentContentNoOptional new content.

Response body

DocumentResponse
{
  "data": {
    "id": "2d4f6a8c-0e1b-4d3f-9a5c-7e9b1d3f5a7c",
    "title": "Roof inspection",
    "kind": "report",
    "status": "draft",
    "archived": false,
    "revision": 3,
    "job_ids": [
      "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10"
    ],
    "customer_id": null,
    "template_id": null,
    "shown_to_customer": false,
    "created_by": "9f2c7e1a-4b3d-4e5f-8a6b-2c1d3e4f5a6b",
    "created_at": "2026-09-28T13:00:00.000Z",
    "updated_at": "2026-09-28T16:00:00.000Z",
    "finalized_at": null,
    "content": {
      "version": 1,
      "frozen": null,
      "page": {
        "size": "letter",
        "orientation": "portrait",
        "margins": "normal"
      },
      "theme": {
        "accentColor": "#1F2937",
        "font": "sans"
      },
      "header": {
        "enabled": false,
        "left": "",
        "center": "",
        "right": "",
        "showLogo": true
      },
      "footer": {
        "enabled": false,
        "text": "",
        "showPageNumbers": true
      },
      "blocks": [
        {
          "id": "b1",
          "type": "heading",
          "level": 1,
          "text": "Roof inspection"
        },
        {
          "id": "b2",
          "type": "rich_text",
          "html": "<p>Two cracked tiles over the garage.</p>"
        }
      ]
    },
    "photo_count": 0,
    "text": "# Roof inspection\n\nTwo cracked tiles over the garage.",
    "text_cut_short": false
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 404 Not found · 409 Conflict · 412 Precondition failed · 429 Rate limited · 413 Request too large · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Invalid request payload.

Usually happens when:
The request body is missing required fields or includes invalid field values.
What to do:
Validate the payload against the generated request schema and fix any `issues` entries before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Creating and changing records needs the Premium plan or above.

Usually happens when:
The company plan includes reading through the API but not writing, or the key lacks the write scope.
What to do:
Upgrade to Premium in Subscription settings, or create a key with the needed write scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

404 Not found

No such document for this key.

Usually happens when:
The document does not exist, is in the trash, is restricted, or belongs to another company.
What to do:
List documents again and use an id from the list.
Error response
{
  "error": "We couldn't find that customer. Check its ID and try again."
}

409 Conflict

The document changed since you read it.

Usually happens when:
Someone saved a newer revision after your last read.
What to do:
Read the document again, apply your change to the new content, and send its revision.
Error response
{
  "error": "A customer already uses this email. Use a different email or open the existing customer.",
  "field": "email"
}

412 Precondition failed

The document cannot take this change through the API.

Usually happens when:
The document is final, or it shows photos the key cannot see and new content was sent.
What to do:
Reopen it as a draft in Asteri, or change only the title.
Error response
{
  "error": "Configure or manually resolve sales tax before publishing."
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle mutation throughput and avoid blind retries from queues or worker pools.
Retry guidance:
Honor the `Retry-After` header and replay writes only after the relevant limit window resets.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

413 Request too large

The JSON request body exceeds this route’s byte limit.

Usually happens when:
The request body is larger than this write route accepts.
What to do:
Reduce the request body and try again.
Error response
{
  "error": "The request is too large. Send a smaller JSON body and try again."
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this write.
What to do:
Check API availability, then retry with the same idempotency key when applicable.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while validating or persisting the mutation.
What to do:
Capture the response payload and contract version, then retry once before escalating.
Error response
{
  "error": "We couldn't load customers. Try again."
}
View documents schemas and field examples

Document

A saved document: a report, daily log, summary or packet built from job photos and job data.

Document fields
FieldTypeRequiredDescription
iduuidYesDocument identifier.
titlestringYesTitle.
kind"report" | "daily_log" | "summary" | "custom" | "warranty_packet" | "insurance_packet"YesWhat kind of document it is.
status"draft" | "final"YesDrafts can be edited; final documents are locked until reopened in Asteri.
archivedbooleanYesArchived documents are listed only with archived=true.
revisionintegerYesSend it as expected_revision when editing.
job_idsuuid[]YesThe jobs it draws from; the first is the job it was made from.
customer_iduuid | nullYesThe customer.
template_iduuid | nullYesThe document template it was made from.
shown_to_customerbooleanYesShown on the customer page once final.
created_byuuidYesThe team member who made it.
created_atdatetime | nullYesWhen it was made.
updated_atdatetime | nullYesLast change.
finalized_atdatetime | nullYesWhen it was made final.
Example Document
{
  "id": "2d4f6a8c-0e1b-4d3f-9a5c-7e9b1d3f5a7c",
  "title": "Roof inspection",
  "kind": "report",
  "status": "draft",
  "archived": false,
  "revision": 3,
  "job_ids": [
    "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10"
  ],
  "customer_id": null,
  "template_id": null,
  "shown_to_customer": false,
  "created_by": "9f2c7e1a-4b3d-4e5f-8a6b-2c1d3e4f5a6b",
  "created_at": "2026-09-28T13:00:00.000Z",
  "updated_at": "2026-09-28T16:00:00.000Z",
  "finalized_at": null
}

DocumentDetail

One document with its blocks and its words. Restricted and deleted photos are left out.

DocumentDetail fields
FieldTypeRequiredDescription
iduuidYesDocument identifier.
titlestringYesTitle.
kind"report" | "daily_log" | "summary" | "custom" | "warranty_packet" | "insurance_packet"YesWhat kind of document it is.
status"draft" | "final"YesDrafts can be edited; final documents are locked until reopened in Asteri.
archivedbooleanYesArchived documents are listed only with archived=true.
revisionintegerYesSend it as expected_revision when editing.
job_idsuuid[]YesThe jobs it draws from; the first is the job it was made from.
customer_iduuid | nullYesThe customer.
template_iduuid | nullYesThe document template it was made from.
shown_to_customerbooleanYesShown on the customer page once final.
created_byuuidYesThe team member who made it.
created_atdatetime | nullYesWhen it was made.
updated_atdatetime | nullYesLast change.
finalized_atdatetime | nullYesWhen it was made final.
contentDocumentContentYesThe blocks.
photo_countintegerYesPhotos in the content.
textstringYesThe words of the document in reading order.
text_cut_shortbooleanYesThe text stops at 50,000 characters.
Example DocumentDetail
{
  "id": "2d4f6a8c-0e1b-4d3f-9a5c-7e9b1d3f5a7c",
  "title": "Roof inspection",
  "kind": "report",
  "status": "draft",
  "archived": false,
  "revision": 3,
  "job_ids": [
    "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10"
  ],
  "customer_id": null,
  "template_id": null,
  "shown_to_customer": false,
  "created_by": "9f2c7e1a-4b3d-4e5f-8a6b-2c1d3e4f5a6b",
  "created_at": "2026-09-28T13:00:00.000Z",
  "updated_at": "2026-09-28T16:00:00.000Z",
  "finalized_at": null,
  "content": {
    "version": 1,
    "frozen": null,
    "page": {
      "size": "letter",
      "orientation": "portrait",
      "margins": "normal"
    },
    "theme": {
      "accentColor": "#1F2937",
      "font": "sans"
    },
    "header": {
      "enabled": false,
      "left": "",
      "center": "",
      "right": "",
      "showLogo": true
    },
    "footer": {
      "enabled": false,
      "text": "",
      "showPageNumbers": true
    },
    "blocks": [
      {
        "id": "b1",
        "type": "heading",
        "level": 1,
        "text": "Roof inspection"
      },
      {
        "id": "b2",
        "type": "rich_text",
        "html": "<p>Two cracked tiles over the garage.</p>"
      }
    ]
  },
  "photo_count": 0,
  "text": "# Roof inspection\n\nTwo cracked tiles over the garage.",
  "text_cut_short": false
}

DocumentContent

The blocks of a document, in the shape the app saves: headings, text (HTML), tables, photo groups, before-and-after sets, job sections and data blocks. Send it back whole to change it.

DocumentContent fields
FieldTypeRequiredDescription
versionintegerYesContent format version (1).
frozenobject | nullYesValues frozen when the document was made final.
pageobjectYesPage size, orientation and margins.
themeobjectYesAccent colour and font.
headerobjectYesPage header.
footerobjectYesPage footer.
blocksobject[]YesBlocks, in order. Each has an id and a type.
Example DocumentContent
{
  "version": 1,
  "frozen": null,
  "page": {
    "size": "letter",
    "orientation": "portrait",
    "margins": "normal"
  },
  "theme": {
    "accentColor": "#1F2937",
    "font": "sans"
  },
  "header": {
    "enabled": false,
    "left": "",
    "center": "",
    "right": "",
    "showLogo": true
  },
  "footer": {
    "enabled": false,
    "text": "",
    "showPageNumbers": true
  },
  "blocks": [
    {
      "id": "b1",
      "type": "heading",
      "level": 1,
      "text": "Roof inspection"
    },
    {
      "id": "b2",
      "type": "rich_text",
      "html": "<p>Two cracked tiles over the garage.</p>"
    }
  ]
}

DocumentListResponse

Documents, newest change first.

DocumentListResponse fields
FieldTypeRequiredDescription
dataDocument[]YesDocuments.
paginationPaginationYesPage information.
Example DocumentListResponse
{
  "data": [
    {
      "id": "2d4f6a8c-0e1b-4d3f-9a5c-7e9b1d3f5a7c",
      "title": "Roof inspection",
      "kind": "report",
      "status": "draft",
      "archived": false,
      "revision": 3,
      "job_ids": [
        "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10"
      ],
      "customer_id": null,
      "template_id": null,
      "shown_to_customer": false,
      "created_by": "9f2c7e1a-4b3d-4e5f-8a6b-2c1d3e4f5a6b",
      "created_at": "2026-09-28T13:00:00.000Z",
      "updated_at": "2026-09-28T16:00:00.000Z",
      "finalized_at": null
    }
  ],
  "pagination": {
    "count": 2,
    "limit": 25,
    "next_cursor": null
  }
}

DocumentResponse

One document.

DocumentResponse fields
FieldTypeRequiredDescription
dataDocumentDetailYesThe document.
Example DocumentResponse
{
  "data": {
    "id": "2d4f6a8c-0e1b-4d3f-9a5c-7e9b1d3f5a7c",
    "title": "Roof inspection",
    "kind": "report",
    "status": "draft",
    "archived": false,
    "revision": 3,
    "job_ids": [
      "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10"
    ],
    "customer_id": null,
    "template_id": null,
    "shown_to_customer": false,
    "created_by": "9f2c7e1a-4b3d-4e5f-8a6b-2c1d3e4f5a6b",
    "created_at": "2026-09-28T13:00:00.000Z",
    "updated_at": "2026-09-28T16:00:00.000Z",
    "finalized_at": null,
    "content": {
      "version": 1,
      "frozen": null,
      "page": {
        "size": "letter",
        "orientation": "portrait",
        "margins": "normal"
      },
      "theme": {
        "accentColor": "#1F2937",
        "font": "sans"
      },
      "header": {
        "enabled": false,
        "left": "",
        "center": "",
        "right": "",
        "showLogo": true
      },
      "footer": {
        "enabled": false,
        "text": "",
        "showPageNumbers": true
      },
      "blocks": [
        {
          "id": "b1",
          "type": "heading",
          "level": 1,
          "text": "Roof inspection"
        },
        {
          "id": "b2",
          "type": "rich_text",
          "html": "<p>Two cracked tiles over the garage.</p>"
        }
      ]
    },
    "photo_count": 0,
    "text": "# Roof inspection\n\nTwo cracked tiles over the garage.",
    "text_cut_short": false
  }
}

DocumentCreateRequest

Make a draft on one job, or a packet over several jobs of one customer. Premium plan and above.

DocumentCreateRequest fields
FieldTypeRequiredDescription
kind"report" | "daily_log" | "summary" | "custom" | "warranty_packet" | "insurance_packet"YesWhat kind of document to make.
jobsDocumentJobSelection[]YesThe jobs, all of one customer; the first is the job it is made from.
titlestringNoOptional title; defaults to the kind and the job or customer.
startDocumentStartNoOptional; the layout for the kind by default.
Example DocumentCreateRequest
{
  "kind": "report",
  "jobs": [
    {
      "job_id": "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10"
    }
  ]
}

DocumentJobSelection

A job of a new document; visit_ids narrows it to some of its visits.

DocumentJobSelection fields
FieldTypeRequiredDescription
job_iduuidYesThe job.
visit_idsuuid[]NoOptional visits of the job.
Example DocumentJobSelection
{
  "job_id": "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10"
}

DocumentStart

How a new document starts: blank, the layout for its kind built from the jobs’ photos (the default), or a company document template.

DocumentStart fields
FieldTypeRequiredDescription
type"blank" | "layout" | "template"YesHow it starts.
template_iduuidNoThe document template, for type "template".
Example DocumentStart
{
  "type": "layout"
}

DocumentUpdateRequest

Edit a draft: a new title, new content (the whole block list, as GET returns it) or both. Premium plan and above.

DocumentUpdateRequest fields
FieldTypeRequiredDescription
expected_revisionintegerYesThe revision you last read.
titlestringNoOptional new title.
contentDocumentContentNoOptional new content.
Example DocumentUpdateRequest
{
  "expected_revision": 3,
  "title": "Roof inspection, garage"
}

Time entries

Time on the clock for payroll and job costing.

/api/public/v1/time-entries
GET
/api/public/v1/time-entriesList time entries newest first; filter by team_member_id, job_id, since and until (clock-in time).

Required permissions

read:time_entries

Success:200

Request parameters

limitQuery · integer · Optional
Maximum records per page, from 1 to 100 (default 25).
cursorQuery · string · Optional
Opaque next_cursor from the previous response.
team_member_idQuery · uuid · Optional
Filter by team member ID.
job_idQuery · uuid · Optional
Filter by job ID.
sinceQuery · datetime · Optional
Earliest clock-in time.
untilQuery · datetime · Optional
Latest clock-in time.

Usage

cURL
curl -X GET \
  'http://localhost:3000/api/public/v1/time-entries' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.timeEntries.list();

Response body

TimeEntryListResponse
{
  "data": [
    {
      "id": "4a6c8e0b-2d4f-4a6c-8e0b-2d4f6a8c0e1b",
      "team_member_id": "9f2c7e1a-4b3d-4e5f-8a6b-2c1d3e4f5a6b",
      "team_member_name": "Chris Lane",
      "start_time": "2026-09-28T12:00:00.000Z",
      "end_time": "2026-09-28T20:30:00.000Z",
      "work_date": "2026-09-28",
      "time_zone": "America/New_York",
      "status": "completed",
      "duration_minutes": 480,
      "break_minutes": 30,
      "entry_type": "work",
      "is_general_entry": false,
      "job_id": "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10",
      "job_visit_id": null,
      "timesheet_code_id": null,
      "timesheet_code_name": null,
      "is_billable": true,
      "auto_clocked_out": false,
      "approval_status": "not_required",
      "clock_in_verified": true,
      "clock_out_verified": true,
      "has_clock_in_photo": false,
      "has_clock_out_photo": false
    }
  ],
  "pagination": {
    "count": 2,
    "limit": 25,
    "next_cursor": null
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 429 Rate limited · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Malformed query or request payload.

Usually happens when:
The request sends malformed query/body data.
What to do:
Validate request fields against the contract examples before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Missing plan access, capability, IP access, or scopes for this request.

Usually happens when:
The key is valid but the company’s plan does not include this (reads need a paid plan, writes need Premium or above), API access is suspended, the key IP allowlist does not match, or the key lacks the required scope.
What to do:
Upgrade the plan for writes, adjust the key IP allowlist, or create a key with the required scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle request volume and avoid immediate fan-out retries from parallel workers.
Retry guidance:
Honor the `Retry-After` header and use exponential backoff for repeated limits.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this request.
What to do:
Check API availability, then retry with backoff.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while reading or serializing the resource.
What to do:
Log the contract version and response body, then retry once before escalating to Asteri support.
Error response
{
  "error": "We couldn't load customers. Try again."
}
View time entries schemas and field examples

TimeEntry

Time on the clock. Never includes photos, locations or pay.

TimeEntry fields
FieldTypeRequiredDescription
iduuidYesTime entry identifier.
team_member_iduuidYesWho was on the clock.
team_member_namestring | nullYesTheir name.
start_timedatetimeYesClock-in time.
end_timedatetime | nullYesClock-out time; null while on the clock.
work_datedate | nullYesThe company day the entry started on.
time_zonestring | nullYesThe company time zone.
statusstring | nullYesEntry status.
duration_minutesinteger | nullYesMinutes worked.
break_minutesintegerYesBreak minutes.
entry_typestring | nullYesEntry type.
is_general_entrybooleanYesTime not on a job.
job_iduuid | nullYesThe job.
job_visit_iduuid | nullYesThe visit.
timesheet_code_iduuid | nullYesTimesheet code.
timesheet_code_namestring | nullYesTimesheet code name.
is_billablebooleanYesBillable time.
auto_clocked_outbooleanYesEnded by the automatic clock-out.
approval_statusstring | nullYesApproval status.
clock_in_verifiedboolean | nullYesClock-in location checked.
clock_out_verifiedboolean | nullYesClock-out location checked.
has_clock_in_photobooleanYesA clock-in photo exists (never returned).
has_clock_out_photobooleanYesA clock-out photo exists (never returned).
Example TimeEntry
{
  "id": "4a6c8e0b-2d4f-4a6c-8e0b-2d4f6a8c0e1b",
  "team_member_id": "9f2c7e1a-4b3d-4e5f-8a6b-2c1d3e4f5a6b",
  "team_member_name": "Chris Lane",
  "start_time": "2026-09-28T12:00:00.000Z",
  "end_time": "2026-09-28T20:30:00.000Z",
  "work_date": "2026-09-28",
  "time_zone": "America/New_York",
  "status": "completed",
  "duration_minutes": 480,
  "break_minutes": 30,
  "entry_type": "work",
  "is_general_entry": false,
  "job_id": "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10",
  "job_visit_id": null,
  "timesheet_code_id": null,
  "timesheet_code_name": null,
  "is_billable": true,
  "auto_clocked_out": false,
  "approval_status": "not_required",
  "clock_in_verified": true,
  "clock_out_verified": true,
  "has_clock_in_photo": false,
  "has_clock_out_photo": false
}

TimeEntryListResponse

Time entries, newest first.

TimeEntryListResponse fields
FieldTypeRequiredDescription
dataTimeEntry[]YesTime entries.
paginationPaginationYesPage information.
Example TimeEntryListResponse
{
  "data": [
    {
      "id": "4a6c8e0b-2d4f-4a6c-8e0b-2d4f6a8c0e1b",
      "team_member_id": "9f2c7e1a-4b3d-4e5f-8a6b-2c1d3e4f5a6b",
      "team_member_name": "Chris Lane",
      "start_time": "2026-09-28T12:00:00.000Z",
      "end_time": "2026-09-28T20:30:00.000Z",
      "work_date": "2026-09-28",
      "time_zone": "America/New_York",
      "status": "completed",
      "duration_minutes": 480,
      "break_minutes": 30,
      "entry_type": "work",
      "is_general_entry": false,
      "job_id": "5b1e4c8a-2f6d-4c3b-9a1e-7d2f8c4b6a10",
      "job_visit_id": null,
      "timesheet_code_id": null,
      "timesheet_code_name": null,
      "is_billable": true,
      "auto_clocked_out": false,
      "approval_status": "not_required",
      "clock_in_verified": true,
      "clock_out_verified": true,
      "has_clock_in_photo": false,
      "has_clock_out_photo": false
    }
  ],
  "pagination": {
    "count": 2,
    "limit": 25,
    "next_cursor": null
  }
}

Webhook subscriptions

REST hooks: subscribe an address to events when a Zap or scenario turns on, remove it when it turns off.

/api/public/v1/webhooks
GET
/api/public/v1/webhooks/eventsList the events a subscription can listen for.

Required permissions

manage:webhooks

Success:200

Usage

cURL
curl -X GET \
  'http://localhost:3000/api/public/v1/webhooks/events' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.webhookSubscriptions.events();

Response body

WebhookEventNameList
{
  "data": [
    {
      "name": "photo.created"
    },
    {
      "name": "photo.tagged"
    },
    {
      "name": "photo.updated"
    },
    {
      "name": "checklist.completed"
    }
  ]
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 429 Rate limited · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Malformed query or request payload.

Usually happens when:
The request sends malformed query/body data.
What to do:
Validate request fields against the contract examples before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Missing plan access, capability, IP access, or scopes for this request.

Usually happens when:
The key is valid but the company’s plan does not include this (reads need a paid plan, writes need Premium or above), API access is suspended, the key IP allowlist does not match, or the key lacks the required scope.
What to do:
Upgrade the plan for writes, adjust the key IP allowlist, or create a key with the required scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle request volume and avoid immediate fan-out retries from parallel workers.
Retry guidance:
Honor the `Retry-After` header and use exponential backoff for repeated limits.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this request.
What to do:
Check API availability, then retry with backoff.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while reading or serializing the resource.
What to do:
Log the contract version and response body, then retry once before escalating to Asteri support.
Error response
{
  "error": "We couldn't load customers. Try again."
}
GET
/api/public/v1/webhooksList this key's subscriptions.

Required permissions

manage:webhooks

Success:200

Usage

cURL
curl -X GET \
  'http://localhost:3000/api/public/v1/webhooks' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.webhookSubscriptions.list();

Response body

WebhookSubscriptionListResponse
{
  "data": [
    {
      "id": "1f3b5d7f-9a1c-4e3f-8b5d-7f9a1c3e5b7d",
      "target_url": "https://hooks.zapier.com/hooks/standard/123/abc",
      "events": [
        "photo.created"
      ],
      "source": "zapier",
      "active": true,
      "created_at": "2026-09-28T14:00:00.000Z",
      "last_delivery_at": null,
      "last_delivery_status": null
    }
  ]
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 429 Rate limited · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Malformed query or request payload.

Usually happens when:
The request sends malformed query/body data.
What to do:
Validate request fields against the contract examples before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Missing plan access, capability, IP access, or scopes for this request.

Usually happens when:
The key is valid but the company’s plan does not include this (reads need a paid plan, writes need Premium or above), API access is suspended, the key IP allowlist does not match, or the key lacks the required scope.
What to do:
Upgrade the plan for writes, adjust the key IP allowlist, or create a key with the required scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle request volume and avoid immediate fan-out retries from parallel workers.
Retry guidance:
Honor the `Retry-After` header and use exponential backoff for repeated limits.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this request.
What to do:
Check API availability, then retry with backoff.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while reading or serializing the resource.
What to do:
Log the contract version and response body, then retry once before escalating to Asteri support.
Error response
{
  "error": "We couldn't load customers. Try again."
}
POST
/api/public/v1/webhooksSubscribe a public https address to events; the signing secret is returned once.

Required permissions

manage:webhooks

Success:201

Usage

cURL
curl -X POST \
  'http://localhost:3000/api/public/v1/webhooks' \
  -H "Authorization: Bearer ast_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{
  "target_url": "https://hooks.zapier.com/hooks/standard/123/abc",
  "events": [
    "photo.created"
  ],
  "source": "zapier"
}'
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.webhookSubscriptions.create({
  "target_url": "https://hooks.zapier.com/hooks/standard/123/abc",
  "events": [
    "photo.created"
  ],
  "source": "zapier"
});

Request body

Subscribe a public https address to events. Premium plan and above.

WebhookSubscriptionRequest
{
  "target_url": "https://hooks.zapier.com/hooks/standard/123/abc",
  "events": [
    "photo.created"
  ],
  "source": "zapier"
}
View request fields
WebhookSubscriptionRequest fields
FieldTypeRequiredDescription
target_urlstringYesPublic https address.
eventsstring[]YesEvents from GET /webhooks/events.
source"api" | "zapier" | "make"NoWhich tool is subscribing.
namestringNoOptional label shown in Settings.

Response body

WebhookSubscriptionCreateResponse
{
  "data": {
    "id": "1f3b5d7f-9a1c-4e3f-8b5d-7f9a1c3e5b7d",
    "target_url": "https://hooks.zapier.com/hooks/standard/123/abc",
    "events": [
      "photo.created"
    ],
    "source": "zapier",
    "active": true,
    "created_at": "2026-09-28T14:00:00.000Z",
    "last_delivery_at": null,
    "last_delivery_status": null,
    "secret": "whsec_4f2a..."
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 429 Rate limited · 413 Request too large · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Invalid request payload.

Usually happens when:
The request body is missing required fields or includes invalid field values.
What to do:
Validate the payload against the generated request schema and fix any `issues` entries before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Creating and changing records needs the Premium plan or above.

Usually happens when:
The company plan includes reading through the API but not writing, or the key lacks the write scope.
What to do:
Upgrade to Premium in Subscription settings, or create a key with the needed write scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle mutation throughput and avoid blind retries from queues or worker pools.
Retry guidance:
Honor the `Retry-After` header and replay writes only after the relevant limit window resets.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

413 Request too large

The JSON request body exceeds this route’s byte limit.

Usually happens when:
The request body is larger than this write route accepts.
What to do:
Reduce the request body and try again.
Error response
{
  "error": "The request is too large. Send a smaller JSON body and try again."
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this write.
What to do:
Check API availability, then retry with the same idempotency key when applicable.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while validating or persisting the mutation.
What to do:
Capture the response payload and contract version, then retry once before escalating.
Error response
{
  "error": "We couldn't load customers. Try again."
}
DELETE
/api/public/v1/webhooks/{subscriptionId}Remove a subscription this key made (answers 204).

Required permissions

manage:webhooks

Success:204204 returns no response body.

Usage

cURL
curl -X DELETE \
  'http://localhost:3000/api/public/v1/webhooks/8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.webhookSubscriptions.delete({ subscriptionId: '8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3' });

Response body

A successful 204 response has no body.

Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 429 Rate limited · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Malformed query or request payload.

Usually happens when:
The request sends malformed query/body data.
What to do:
Validate request fields against the contract examples before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Missing plan access, capability, IP access, or scopes for this request.

Usually happens when:
The key is valid but the company’s plan does not include this (reads need a paid plan, writes need Premium or above), API access is suspended, the key IP allowlist does not match, or the key lacks the required scope.
What to do:
Upgrade the plan for writes, adjust the key IP allowlist, or create a key with the required scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle request volume and avoid immediate fan-out retries from parallel workers.
Retry guidance:
Honor the `Retry-After` header and use exponential backoff for repeated limits.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this request.
What to do:
Check API availability, then retry with backoff.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while reading or serializing the resource.
What to do:
Log the contract version and response body, then retry once before escalating to Asteri support.
Error response
{
  "error": "We couldn't load customers. Try again."
}
View webhook subscriptions schemas and field examples

WebhookSubscription

An event subscription made through the API (Zapier, Make or your own tool).

WebhookSubscription fields
FieldTypeRequiredDescription
iduuidYesSubscription identifier.
target_urlstringYesWhere events are delivered.
eventsstring[]YesEvents delivered.
source"api" | "zapier" | "make"YesWhich tool made it.
activebooleanYesWhether events are delivered.
created_atdatetimeYesWhen it was made.
last_delivery_atdatetime | nullYesLast delivery attempt.
last_delivery_statusstring | nullYesLast delivery result.
Example WebhookSubscription
{
  "id": "1f3b5d7f-9a1c-4e3f-8b5d-7f9a1c3e5b7d",
  "target_url": "https://hooks.zapier.com/hooks/standard/123/abc",
  "events": [
    "photo.created"
  ],
  "source": "zapier",
  "active": true,
  "created_at": "2026-09-28T14:00:00.000Z",
  "last_delivery_at": null,
  "last_delivery_status": null
}

WebhookSubscriptionRequest

Subscribe a public https address to events. Premium plan and above.

WebhookSubscriptionRequest fields
FieldTypeRequiredDescription
target_urlstringYesPublic https address.
eventsstring[]YesEvents from GET /webhooks/events.
source"api" | "zapier" | "make"NoWhich tool is subscribing.
namestringNoOptional label shown in Settings.
Example WebhookSubscriptionRequest
{
  "target_url": "https://hooks.zapier.com/hooks/standard/123/abc",
  "events": [
    "photo.created"
  ],
  "source": "zapier"
}

WebhookSubscriptionCreateResponse

The new subscription and its signing secret (shown only now).

WebhookSubscriptionCreateResponse fields
FieldTypeRequiredDescription
dataWebhookSubscription & { secret }YesThe subscription with `secret` for checking the X-Asteri-Signature header.
Example WebhookSubscriptionCreateResponse
{
  "data": {
    "id": "1f3b5d7f-9a1c-4e3f-8b5d-7f9a1c3e5b7d",
    "target_url": "https://hooks.zapier.com/hooks/standard/123/abc",
    "events": [
      "photo.created"
    ],
    "source": "zapier",
    "active": true,
    "created_at": "2026-09-28T14:00:00.000Z",
    "last_delivery_at": null,
    "last_delivery_status": null,
    "secret": "whsec_4f2a..."
  }
}

Webhook Events

Signed event delivery with retries, delivery history, secret rotation, and replay-oriented operations.

/developers/webhooks
EVENT
customer.created | customer.updated | customer.deletedCustomer lifecycle events delivered to subscribed webhook endpoints.

Required permissions

manage:webhooks

Response body

WebhookEventPayload
{
  "event": "customer.created",
  "timestamp": "2026-03-20T14:30:00.000Z",
  "data": {
    "id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
    "organization_id": "dce3853d-4f94-4bc8-a816-4d0d74c11a9d"
  }
}
EVENT
appointment.created | appointment.updated | appointment.completed | appointment.cancelledAppointment lifecycle events delivered with signed payloads.

Required permissions

manage:webhooks

Response body

WebhookEventPayload
{
  "event": "customer.created",
  "timestamp": "2026-03-20T14:30:00.000Z",
  "data": {
    "id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
    "organization_id": "dce3853d-4f94-4bc8-a816-4d0d74c11a9d"
  }
}
EVENT
photo.createdA job photo or video finished uploading, from any app, import or upload. Carries the Photo with a download link that lasts a day.

Required permissions

manage:webhooks

Response body

WebhookEventPayload
{
  "event": "customer.created",
  "timestamp": "2026-03-20T14:30:00.000Z",
  "data": {
    "id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
    "organization_id": "dce3853d-4f94-4bc8-a816-4d0d74c11a9d"
  }
}
EVENT
photo.tagged | photo.updatedA job photo's tags were added or removed (added_tags, removed_tags), or its caption, stage, area, customer page visibility or markup changed (changes, area, customer_visibility, markup with a day-long image link). Each carries the Photo as it is now.

Required permissions

manage:webhooks

Response body

WebhookEventPayload
{
  "event": "customer.created",
  "timestamp": "2026-03-20T14:30:00.000Z",
  "data": {
    "id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
    "organization_id": "dce3853d-4f94-4bc8-a816-4d0d74c11a9d"
  }
}
EVENT
checklist.completed | report.completed | time_entry.clocked_outA checklist's last step was done, a photo report was completed, or time on the clock ended.

Required permissions

manage:webhooks

Response body

WebhookEventPayload
{
  "event": "customer.created",
  "timestamp": "2026-03-20T14:30:00.000Z",
  "data": {
    "id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
    "organization_id": "dce3853d-4f94-4bc8-a816-4d0d74c11a9d"
  }
}
View webhook events schemas and field examples

WebhookEventPayload

Signed webhook payload delivered to subscribers.

WebhookEventPayload fields
FieldTypeRequiredDescription
eventstringYesEvent name for the lifecycle change that occurred.
timestampdatetimeYesISO-8601 event timestamp.
dataobjectYesEntity payload associated with the event.
Example WebhookEventPayload
{
  "event": "customer.created",
  "timestamp": "2026-03-20T14:30:00.000Z",
  "data": {
    "id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
    "organization_id": "dce3853d-4f94-4bc8-a816-4d0d74c11a9d"
  }
}

Conversations

Inbox threads with customers, and the messages in each thread.

/api/public/v1/conversations
GET
/api/public/v1/conversationsList conversations in the active company. Filters: status (open, closed, snoozed), customer_id. Pagination: limit (1-100, default 25) and offset, with next_cursor.

Required permissions

conversations:read

Success:200

Usage

cURL
curl -X GET \
  'http://localhost:3000/api/public/v1/conversations' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.conversations.list();

Response body

ConversationListResponse
{
  "data": [
    {
      "id": "3f6e2c1a-8b7d-4c9e-9a1b-2d3e4f5a6b7c",
      "customer_id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
      "customer_phone": "+14105550101",
      "status": "open",
      "last_message_at": "2026-10-22T14:00:00.000Z",
      "last_message_preview": "Can you come out Tuesday morning?",
      "unread_count": 2,
      "assigned_to": null,
      "created_at": "2026-10-01T09:00:00.000Z"
    }
  ],
  "pagination": {
    "count": 2,
    "limit": 25,
    "next_cursor": null
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 429 Rate limited · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Malformed query or request payload.

Usually happens when:
The request sends malformed query/body data.
What to do:
Validate request fields against the contract examples before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Missing plan access, capability, IP access, or scopes for this request.

Usually happens when:
The key is valid but the company’s plan does not include this (reads need a paid plan, writes need Premium or above), API access is suspended, the key IP allowlist does not match, or the key lacks the required scope.
What to do:
Upgrade the plan for writes, adjust the key IP allowlist, or create a key with the required scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle request volume and avoid immediate fan-out retries from parallel workers.
Retry guidance:
Honor the `Retry-After` header and use exponential backoff for repeated limits.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this request.
What to do:
Check API availability, then retry with backoff.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while reading or serializing the resource.
What to do:
Log the contract version and response body, then retry once before escalating to Asteri support.
Error response
{
  "error": "We couldn't load customers. Try again."
}
GET
/api/public/v1/conversations/{conversationId}Read one conversation and a page of its messages, newest first. Query: limit (1-100, default 50) and offset for the messages.

Required permissions

conversations:read

Success:200

Usage

cURL
curl -X GET \
  'http://localhost:3000/api/public/v1/conversations/8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.conversations.get({ conversationId: '8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3' });

Response body

ConversationThreadResponse
{
  "data": {
    "id": "3f6e2c1a-8b7d-4c9e-9a1b-2d3e4f5a6b7c",
    "customer_id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
    "customer_phone": "+14105550101",
    "status": "open",
    "last_message_at": "2026-10-22T14:00:00.000Z",
    "last_message_preview": "Can you come out Tuesday morning?",
    "unread_count": 2,
    "assigned_to": null,
    "created_at": "2026-10-01T09:00:00.000Z",
    "messages": [
      {
        "id": "7d1b0c9e-2f4a-4b8e-9c3d-5a6b7c8d9e0f",
        "direction": "inbound",
        "channel": "sms",
        "sender_type": "customer",
        "status": "delivered",
        "content": "Can you come out Tuesday morning?",
        "subject": null,
        "created_at": "2026-10-22T14:00:00.000Z"
      }
    ]
  },
  "pagination": {
    "count": 2,
    "limit": 25,
    "next_cursor": null
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 404 Not found · 429 Rate limited · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Malformed query or request payload.

Usually happens when:
The request sends malformed query/body data.
What to do:
Validate request fields against the contract examples before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Missing plan access, capability, IP access, or scopes for this request.

Usually happens when:
The key is valid but the company’s plan does not include this (reads need a paid plan, writes need Premium or above), API access is suspended, the key IP allowlist does not match, or the key lacks the required scope.
What to do:
Upgrade the plan for writes, adjust the key IP allowlist, or create a key with the required scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

404 Not found

No conversation with this ID exists in the active company.

Usually happens when:
The conversation ID belongs to another company, was never created, or is mistyped.
What to do:
List conversations first and use an ID from that list.
Error response
{
  "error": "We couldn't find that customer. Check its ID and try again."
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle request volume and avoid immediate fan-out retries from parallel workers.
Retry guidance:
Honor the `Retry-After` header and use exponential backoff for repeated limits.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this request.
What to do:
Check API availability, then retry with backoff.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while reading or serializing the resource.
What to do:
Log the contract version and response body, then retry once before escalating to Asteri support.
Error response
{
  "error": "We couldn't load customers. Try again."
}
View conversations schemas and field examples

Conversation

One inbox thread with a customer. The thread never includes internal team notes.

Conversation fields
FieldTypeRequiredDescription
iduuidYesConversation ID.
customer_iduuid | nullYesThe customer this thread belongs to. Null until the thread is linked to a customer.
customer_phonestring | nullYesPhone number the thread is with, in E.164 format.
statusopen | closed | snoozedYesWhere the thread sits in the inbox.
last_message_atdate-time | nullYesWhen the newest message in the thread was sent or received.
last_message_previewstring | nullYesThe start of the newest message, for list views.
unread_countintegerYesMessages the company has not opened yet.
assigned_touuid | nullYesTeam member the thread is assigned to, or null when no one has it.
created_atdate-timeYesWhen the thread was created.
Example Conversation
{
  "id": "3f6e2c1a-8b7d-4c9e-9a1b-2d3e4f5a6b7c",
  "customer_id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
  "customer_phone": "+14105550101",
  "status": "open",
  "last_message_at": "2026-10-22T14:00:00.000Z",
  "last_message_preview": "Can you come out Tuesday morning?",
  "unread_count": 2,
  "assigned_to": null,
  "created_at": "2026-10-01T09:00:00.000Z"
}

ConversationMessage

One text or email in a conversation. Internal notes are never returned. Storage details and provider metadata are not included.

ConversationMessage fields
FieldTypeRequiredDescription
iduuidYesMessage ID.
directioninbound | outboundYesinbound for a message from the customer; outbound for one the company sent.
channelstringYesHow the message travelled: sms, mms, or email, and other inbox channels.
sender_typestringYesWho wrote it: customer, team_member, system (an automated or API send), ai, or external (an email from an address that is not on the customer’s record).
statusstring | nullYesDelivery state, such as queued, sending, sent, delivered, or failed.
contentstringYesThe text of the message.
subjectstring | nullYesThe subject line of an email. Null for a text.
created_atdate-timeYesWhen the message was created.
Example ConversationMessage
{
  "id": "7d1b0c9e-2f4a-4b8e-9c3d-5a6b7c8d9e0f",
  "direction": "inbound",
  "channel": "sms",
  "sender_type": "customer",
  "status": "delivered",
  "content": "Can you come out Tuesday morning?",
  "subject": null,
  "created_at": "2026-10-22T14:00:00.000Z"
}

ConversationListResponse

Response body for the conversation list, most recent activity first.

ConversationListResponse fields
FieldTypeRequiredDescription
dataConversation[]YesConversations of the active company that match the filters.
paginationPaginationYesList metadata for page traversal.
Example ConversationListResponse
{
  "data": [
    {
      "id": "3f6e2c1a-8b7d-4c9e-9a1b-2d3e4f5a6b7c",
      "customer_id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
      "customer_phone": "+14105550101",
      "status": "open",
      "last_message_at": "2026-10-22T14:00:00.000Z",
      "last_message_preview": "Can you come out Tuesday morning?",
      "unread_count": 2,
      "assigned_to": null,
      "created_at": "2026-10-01T09:00:00.000Z"
    }
  ],
  "pagination": {
    "count": 2,
    "limit": 25,
    "next_cursor": null
  }
}

ConversationThreadResponse

One conversation with a page of its messages, newest first. The pagination block describes the messages.

ConversationThreadResponse fields
FieldTypeRequiredDescription
dataConversationYesThe conversation, with its messages on this page.
paginationPaginationYesList metadata for the messages on this page.
Example ConversationThreadResponse
{
  "data": {
    "id": "3f6e2c1a-8b7d-4c9e-9a1b-2d3e4f5a6b7c",
    "customer_id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
    "customer_phone": "+14105550101",
    "status": "open",
    "last_message_at": "2026-10-22T14:00:00.000Z",
    "last_message_preview": "Can you come out Tuesday morning?",
    "unread_count": 2,
    "assigned_to": null,
    "created_at": "2026-10-01T09:00:00.000Z",
    "messages": [
      {
        "id": "7d1b0c9e-2f4a-4b8e-9c3d-5a6b7c8d9e0f",
        "direction": "inbound",
        "channel": "sms",
        "sender_type": "customer",
        "status": "delivered",
        "content": "Can you come out Tuesday morning?",
        "subject": null,
        "created_at": "2026-10-22T14:00:00.000Z"
      }
    ]
  },
  "pagination": {
    "count": 2,
    "limit": 25,
    "next_cursor": null
  }
}

Messages

Send a text or a one-to-one email to a customer in a conversation.

/api/public/v1/messages
POST
/api/public/v1/messagesSend a text (sms) or an email to a conversation. Requires the Idempotency-Key header (8 to 128 characters). Returns 201 when sent, 202 when the texting service holds the text and sends it when it recovers, and 200 with the stored message for a retry.

Required permissions

messages:write

Success:201

Usage

cURL
curl -X POST \
  'http://localhost:3000/api/public/v1/messages' \
  -H "Authorization: Bearer ast_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{
  "conversation_id": "3f6e2c1a-8b7d-4c9e-9a1b-2d3e4f5a6b7c",
  "channel": "sms",
  "body": "We are on the way, about 20 minutes out.",
  "subject": "Your appointment",
  "message_type": "transactional",
  "attachment_file_ids": []
}'
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.messages.send({
  "conversation_id": "3f6e2c1a-8b7d-4c9e-9a1b-2d3e4f5a6b7c",
  "channel": "sms",
  "body": "We are on the way, about 20 minutes out.",
  "subject": "Your appointment",
  "message_type": "transactional",
  "attachment_file_ids": []
});

Request body

Send one text or one one-to-one email to a conversation. Send an Idempotency-Key header too, so a retry cannot send twice. Unknown fields are refused.

MessageSendRequest
{
  "conversation_id": "3f6e2c1a-8b7d-4c9e-9a1b-2d3e4f5a6b7c",
  "channel": "sms",
  "body": "We are on the way, about 20 minutes out.",
  "subject": "Your appointment",
  "message_type": "transactional",
  "attachment_file_ids": []
}
View request fields
MessageSendRequest fields
FieldTypeRequiredDescription
conversation_iduuidYesThe conversation to send to. The customer on it must be linked and have a phone or email.
channelsms | emailYesSend as a text (sms) or as an email.
bodystringYesThe message. A text is at most 1,600 characters; an email at most 10,000.
subjectstringNoEmail subject line, up to 200 characters. Required for email; not allowed for a text.
message_typetransactional | marketingNotransactional for a reply or service notice (the default). marketing needs the customer’s consent and follows quiet hours. Only texts can be marketing; email is always one-to-one.
attachment_file_idsuuid[]NoFiles from the company library to attach to an email (PDF or image, up to 10 MB together). Not allowed for a text.

Response body

MessageSendResponse
{
  "data": {
    "id": "7d1b0c9e-2f4a-4b8e-9c3d-5a6b7c8d9e0f",
    "direction": "inbound",
    "channel": "sms",
    "sender_type": "customer",
    "status": "delivered",
    "content": "Can you come out Tuesday morning?",
    "subject": null,
    "created_at": "2026-10-22T14:00:00.000Z"
  },
  "deduplicated": false
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 404 Not found · 409 Conflict · 422 Not sent · 503 Temporarily unavailable · 429 Rate limited · 413 Request too large · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Invalid request payload.

Usually happens when:
The request body is missing required fields or includes invalid field values.
What to do:
Validate the payload against the generated request schema and fix any `issues` entries before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Creating and changing records needs the Premium plan or above.

Usually happens when:
The company plan includes reading through the API but not writing, or the key lacks the write scope.
What to do:
Upgrade to Premium in Subscription settings, or create a key with the needed write scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

404 Not found

The conversation is not in the active company.

Usually happens when:
The conversation ID belongs to another company or does not exist.
What to do:
Use a conversation ID from the conversations list.
Error response
{
  "error": "We couldn't find that customer. Check its ID and try again."
}

409 Conflict

The Idempotency-Key was already used for a different message.

Usually happens when:
A retry changed the text or the subject from the first attempt.
What to do:
Use a new Idempotency-Key for a different message.
Error response
{
  "error": "A customer already uses this email. Use a different email or open the existing customer.",
  "field": "email"
}

422 Not sent

Consent, do-not-contact, quiet hours, a missing phone number or email, or texting not set up stopped the message.

Usually happens when:
The customer opted out or is on the do-not-contact list, a marketing text falls in quiet hours, the customer has no phone or email, or the company has not finished texting registration.
What to do:
Read the message in the error and fix the cause. Nothing was sent, so a new Idempotency-Key is not needed once the cause is fixed.
Error response
{
  "error": "This customer has opted out of texts, so this text wasn't sent."
}

503 Temporarily unavailable

The texting service could not take the text right now.

Usually happens when:
The carrier or texting service failed while the text was handed over.
What to do:
Wait a few minutes, then send again with a new Idempotency-Key. The first attempt is marked failed.
Error response
{
  "error": "The text couldn’t be sent right now. Try again in a few minutes."
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle mutation throughput and avoid blind retries from queues or worker pools.
Retry guidance:
Honor the `Retry-After` header and replay writes only after the relevant limit window resets.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

413 Request too large

The JSON request body exceeds this route’s byte limit.

Usually happens when:
The request body is larger than this write route accepts.
What to do:
Reduce the request body and try again.
Error response
{
  "error": "The request is too large. Send a smaller JSON body and try again."
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this write.
What to do:
Check API availability, then retry with the same idempotency key when applicable.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while validating or persisting the mutation.
What to do:
Capture the response payload and contract version, then retry once before escalating.
Error response
{
  "error": "We couldn't load customers. Try again."
}
View messages schemas and field examples

MessageSendRequest

Send one text or one one-to-one email to a conversation. Send an Idempotency-Key header too, so a retry cannot send twice. Unknown fields are refused.

MessageSendRequest fields
FieldTypeRequiredDescription
conversation_iduuidYesThe conversation to send to. The customer on it must be linked and have a phone or email.
channelsms | emailYesSend as a text (sms) or as an email.
bodystringYesThe message. A text is at most 1,600 characters; an email at most 10,000.
subjectstringNoEmail subject line, up to 200 characters. Required for email; not allowed for a text.
message_typetransactional | marketingNotransactional for a reply or service notice (the default). marketing needs the customer’s consent and follows quiet hours. Only texts can be marketing; email is always one-to-one.
attachment_file_idsuuid[]NoFiles from the company library to attach to an email (PDF or image, up to 10 MB together). Not allowed for a text.
Example MessageSendRequest
{
  "conversation_id": "3f6e2c1a-8b7d-4c9e-9a1b-2d3e4f5a6b7c",
  "channel": "sms",
  "body": "We are on the way, about 20 minutes out.",
  "subject": "Your appointment",
  "message_type": "transactional",
  "attachment_file_ids": []
}

MessageSendResponse

The message the company stored. deduplicated is true when this is a retry of a send that already happened.

MessageSendResponse fields
FieldTypeRequiredDescription
dataConversationMessageYesThe stored message.
deduplicatedbooleanYesTrue when the Idempotency-Key matched an earlier send, so nothing was sent again.
Example MessageSendResponse
{
  "data": {
    "id": "7d1b0c9e-2f4a-4b8e-9c3d-5a6b7c8d9e0f",
    "direction": "inbound",
    "channel": "sms",
    "sender_type": "customer",
    "status": "delivered",
    "content": "Can you come out Tuesday morning?",
    "subject": null,
    "created_at": "2026-10-22T14:00:00.000Z"
  },
  "deduplicated": false
}

MessageRefusedResponse

Response when texting or email rules stop the message. Nothing was sent.

MessageRefusedResponse fields
FieldTypeRequiredDescription
errorstringYesPlain sentence saying why the message was not sent and what to do next.
Example MessageRefusedResponse
{
  "error": "This customer has opted out of texts, so this text wasn't sent."
}

MessageUnavailableResponse

Response when the carrier could not take the text right now.

MessageUnavailableResponse fields
FieldTypeRequiredDescription
errorstringYesPlain sentence saying the text could not be sent right now.
Example MessageUnavailableResponse
{
  "error": "The text couldn’t be sent right now. Try again in a few minutes."
}

Deals

Sales deals and pipeline columns: list, create, update, and move deals between columns.

/api/public/v1/deals
GET
/api/public/v1/dealsList deals in the active organization, filtered by pipeline, column, status, or customer, with cursor paging.

Required permissions

deals:read

Success:200

Usage

cURL
curl -X GET \
  'http://localhost:3000/api/public/v1/deals' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.deals.list();

Response body

DealListResponse
{
  "data": [
    {
      "id": "5b0e8f2a-7c1d-4a6e-9f3b-2d8c4e1a6b70",
      "pipeline_id": "0c3d9e71-8b2a-4f5e-a6d4-7e1b9c0f3a52",
      "customer_id": "8e2f4a6b-1d3c-4b7e-9a0f-5c6d8e2b1a93",
      "title": "Kitchen remodel",
      "deal_type": "residential",
      "stage": "estimate_sent",
      "value": 4200,
      "priority": "medium",
      "assigned_to": "3a7d1e5c-9f4b-4c2a-8e6d-1b0f7a3c5d24",
      "expected_close_date": "2026-11-15",
      "lead_source": "Referral",
      "notes": "Wants the quote before Friday.",
      "contact_name": "Dana Ruiz",
      "contact_email": "dana@example.com",
      "contact_phone": "+15555550100",
      "site_name": "Main house",
      "service_address_line_1": "125 Oak Street",
      "service_address_line_2": null,
      "service_city": "Springfield",
      "service_state": "IL",
      "service_postal_code": "62704",
      "won_date": null,
      "lost_date": null,
      "lost_reason": null,
      "metadata": {},
      "stage_entered_at": "2026-10-20T14:30:00.000Z",
      "created_at": "2026-10-18T09:00:00.000Z",
      "updated_at": "2026-10-20T14:30:00.000Z"
    }
  ],
  "pagination": {
    "count": 1,
    "limit": 25,
    "next_cursor": null
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 429 Rate limited · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Malformed query or request payload.

Usually happens when:
The request sends malformed query/body data.
What to do:
Validate request fields against the contract examples before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Missing plan access, capability, IP access, or scopes for this request.

Usually happens when:
The key is valid but the company’s plan does not include this (reads need a paid plan, writes need Premium or above), API access is suspended, the key IP allowlist does not match, or the key lacks the required scope.
What to do:
Upgrade the plan for writes, adjust the key IP allowlist, or create a key with the required scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle request volume and avoid immediate fan-out retries from parallel workers.
Retry guidance:
Honor the `Retry-After` header and use exponential backoff for repeated limits.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this request.
What to do:
Check API availability, then retry with backoff.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while reading or serializing the resource.
What to do:
Log the contract version and response body, then retry once before escalating to Asteri support.
Error response
{
  "error": "We couldn't load customers. Try again."
}
POST
/api/public/v1/dealsCreate a deal on a pipeline. Send an Idempotency-Key header; a retry with the same key and body returns the same deal.

Required permissions

deals:write

Success:201

Usage

cURL
curl -X POST \
  'http://localhost:3000/api/public/v1/deals' \
  -H "Authorization: Bearer ast_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{
  "pipeline_id": "0c3d9e71-8b2a-4f5e-a6d4-7e1b9c0f3a52",
  "title": "Kitchen remodel",
  "deal_type": "residential",
  "stage": "new_lead",
  "value": 4200,
  "priority": "medium",
  "customer_id": "8e2f4a6b-1d3c-4b7e-9a0f-5c6d8e2b1a93",
  "assigned_to": "3a7d1e5c-9f4b-4c2a-8e6d-1b0f7a3c5d24",
  "expected_close_date": "2026-11-15",
  "lead_source": "Referral",
  "notes": "Wants the quote before Friday.",
  "contact_name": "Dana Ruiz",
  "contact_email": "dana@example.com",
  "contact_phone": "+15555550100",
  "site_name": "Main house",
  "service_address_line_1": "125 Oak Street",
  "service_address_line_2": null,
  "service_city": "Springfield",
  "service_state": "IL",
  "service_postal_code": "62704",
  "metadata": {}
}'
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.deals.create({
  "pipeline_id": "0c3d9e71-8b2a-4f5e-a6d4-7e1b9c0f3a52",
  "title": "Kitchen remodel",
  "deal_type": "residential",
  "stage": "new_lead",
  "value": 4200,
  "priority": "medium",
  "customer_id": "8e2f4a6b-1d3c-4b7e-9a0f-5c6d8e2b1a93",
  "assigned_to": "3a7d1e5c-9f4b-4c2a-8e6d-1b0f7a3c5d24",
  "expected_close_date": "2026-11-15",
  "lead_source": "Referral",
  "notes": "Wants the quote before Friday.",
  "contact_name": "Dana Ruiz",
  "contact_email": "dana@example.com",
  "contact_phone": "+15555550100",
  "site_name": "Main house",
  "service_address_line_1": "125 Oak Street",
  "service_address_line_2": null,
  "service_city": "Springfield",
  "service_state": "IL",
  "service_postal_code": "62704",
  "metadata": {}
});

Request body

Fields for a new deal. pipeline_id and title are required; the rest are optional.

DealCreateRequest
{
  "pipeline_id": "0c3d9e71-8b2a-4f5e-a6d4-7e1b9c0f3a52",
  "title": "Kitchen remodel",
  "deal_type": "residential",
  "stage": "new_lead",
  "value": 4200,
  "priority": "medium",
  "customer_id": "8e2f4a6b-1d3c-4b7e-9a0f-5c6d8e2b1a93",
  "assigned_to": "3a7d1e5c-9f4b-4c2a-8e6d-1b0f7a3c5d24",
  "expected_close_date": "2026-11-15",
  "lead_source": "Referral",
  "notes": "Wants the quote before Friday.",
  "contact_name": "Dana Ruiz",
  "contact_email": "dana@example.com",
  "contact_phone": "+15555550100",
  "site_name": "Main house",
  "service_address_line_1": "125 Oak Street",
  "service_address_line_2": null,
  "service_city": "Springfield",
  "service_state": "IL",
  "service_postal_code": "62704",
  "metadata": {}
}
View request fields
DealCreateRequest fields
FieldTypeRequiredDescription
pipeline_iduuidYesPipeline to put the deal on. Ids come from GET /api/public/v1/pipelines.
titlestringYesDeal name, up to 200 characters.
deal_type"commercial" | "residential"NoBusiness or home deal. Defaults to commercial.
stagestringNoStage id to start in. Defaults to the first open column of the pipeline.
valuenumberNoExpected value in dollars. Defaults to 0.
priority"low" | "medium" | "high" | "urgent"NoPriority. Defaults to medium.
customer_iduuid | nullNoCustomer in this company to link.
assigned_touuid | nullNoTeam member in this company who owns the deal.
expected_close_datedate | nullNoExpected close date (YYYY-MM-DD).
lead_sourcestring | nullNoWhere the deal came from, up to 100 characters.
notesstring | nullNoNotes about the deal.
contact_namestring | nullNoContact name.
contact_emailstring | nullNoContact email.
contact_phonestring | nullNoContact phone.
site_namestring | nullNoJob site name.
service_address_line_1string | nullNoJob site street address, line 1.
service_address_line_2string | nullNoJob site street address, line 2.
service_citystring | nullNoJob site city.
service_statestring | nullNoJob site state.
service_postal_codestring | nullNoJob site postal code.
metadataobjectNoExtra deal details, such as the deal profile.

Response body

DealResponse
{
  "data": {
    "id": "5b0e8f2a-7c1d-4a6e-9f3b-2d8c4e1a6b70",
    "pipeline_id": "0c3d9e71-8b2a-4f5e-a6d4-7e1b9c0f3a52",
    "customer_id": "8e2f4a6b-1d3c-4b7e-9a0f-5c6d8e2b1a93",
    "title": "Kitchen remodel",
    "deal_type": "residential",
    "stage": "estimate_sent",
    "value": 4200,
    "priority": "medium",
    "assigned_to": "3a7d1e5c-9f4b-4c2a-8e6d-1b0f7a3c5d24",
    "expected_close_date": "2026-11-15",
    "lead_source": "Referral",
    "notes": "Wants the quote before Friday.",
    "contact_name": "Dana Ruiz",
    "contact_email": "dana@example.com",
    "contact_phone": "+15555550100",
    "site_name": "Main house",
    "service_address_line_1": "125 Oak Street",
    "service_address_line_2": null,
    "service_city": "Springfield",
    "service_state": "IL",
    "service_postal_code": "62704",
    "won_date": null,
    "lost_date": null,
    "lost_reason": null,
    "metadata": {},
    "stage_entered_at": "2026-10-20T14:30:00.000Z",
    "created_at": "2026-10-18T09:00:00.000Z",
    "updated_at": "2026-10-20T14:30:00.000Z"
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 404 Not found · 409 Conflict · 429 Rate limited · 413 Request too large · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Invalid request payload.

Usually happens when:
The request body is missing required fields or includes invalid field values.
What to do:
Validate the payload against the generated request schema and fix any `issues` entries before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Creating and changing records needs the Premium plan or above.

Usually happens when:
The company plan includes reading through the API but not writing, or the key lacks the write scope.
What to do:
Upgrade to Premium in Subscription settings, or create a key with the needed write scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

404 Not found

The deal or pipeline was not found in this company.

Usually happens when:
The id belongs to another company, was deleted, or is not a valid id.
What to do:
Check the id with GET /api/public/v1/deals or GET /api/public/v1/pipelines.
Error response
{
  "error": "We couldn't find that customer. Check its ID and try again."
}

409 Conflict

This Idempotency-Key was already used for a different request body.

Usually happens when:
A retry changed the request body, or the key was reused for a new deal.
What to do:
Send the original body again with the same key, or use a new Idempotency-Key for a new deal.
Error response
{
  "error": "A customer already uses this email. Use a different email or open the existing customer.",
  "field": "email"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle mutation throughput and avoid blind retries from queues or worker pools.
Retry guidance:
Honor the `Retry-After` header and replay writes only after the relevant limit window resets.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

413 Request too large

The JSON request body exceeds this route’s byte limit.

Usually happens when:
The request body is larger than this write route accepts.
What to do:
Reduce the request body and try again.
Error response
{
  "error": "The request is too large. Send a smaller JSON body and try again."
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this write.
What to do:
Check API availability, then retry with the same idempotency key when applicable.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while validating or persisting the mutation.
What to do:
Capture the response payload and contract version, then retry once before escalating.
Error response
{
  "error": "We couldn't load customers. Try again."
}
GET
/api/public/v1/deals/{dealId}Read one deal in the active organization.

Required permissions

deals:read

Success:200

Usage

cURL
curl -X GET \
  'http://localhost:3000/api/public/v1/deals/8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.deals.get({ dealId: '8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3' });

Response body

DealResponse
{
  "data": {
    "id": "5b0e8f2a-7c1d-4a6e-9f3b-2d8c4e1a6b70",
    "pipeline_id": "0c3d9e71-8b2a-4f5e-a6d4-7e1b9c0f3a52",
    "customer_id": "8e2f4a6b-1d3c-4b7e-9a0f-5c6d8e2b1a93",
    "title": "Kitchen remodel",
    "deal_type": "residential",
    "stage": "estimate_sent",
    "value": 4200,
    "priority": "medium",
    "assigned_to": "3a7d1e5c-9f4b-4c2a-8e6d-1b0f7a3c5d24",
    "expected_close_date": "2026-11-15",
    "lead_source": "Referral",
    "notes": "Wants the quote before Friday.",
    "contact_name": "Dana Ruiz",
    "contact_email": "dana@example.com",
    "contact_phone": "+15555550100",
    "site_name": "Main house",
    "service_address_line_1": "125 Oak Street",
    "service_address_line_2": null,
    "service_city": "Springfield",
    "service_state": "IL",
    "service_postal_code": "62704",
    "won_date": null,
    "lost_date": null,
    "lost_reason": null,
    "metadata": {},
    "stage_entered_at": "2026-10-20T14:30:00.000Z",
    "created_at": "2026-10-18T09:00:00.000Z",
    "updated_at": "2026-10-20T14:30:00.000Z"
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 404 Not found · 429 Rate limited · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Malformed query or request payload.

Usually happens when:
The request sends malformed query/body data.
What to do:
Validate request fields against the contract examples before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Missing plan access, capability, IP access, or scopes for this request.

Usually happens when:
The key is valid but the company’s plan does not include this (reads need a paid plan, writes need Premium or above), API access is suspended, the key IP allowlist does not match, or the key lacks the required scope.
What to do:
Upgrade the plan for writes, adjust the key IP allowlist, or create a key with the required scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

404 Not found

The deal or pipeline was not found in this company.

Usually happens when:
The id belongs to another company, was deleted, or is not a valid id.
What to do:
Check the id with GET /api/public/v1/deals or GET /api/public/v1/pipelines.
Error response
{
  "error": "We couldn't find that customer. Check its ID and try again."
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle request volume and avoid immediate fan-out retries from parallel workers.
Retry guidance:
Honor the `Retry-After` header and use exponential backoff for repeated limits.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this request.
What to do:
Check API availability, then retry with backoff.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while reading or serializing the resource.
What to do:
Log the contract version and response body, then retry once before escalating to Asteri support.
Error response
{
  "error": "We couldn't load customers. Try again."
}
PATCH
/api/public/v1/deals/{dealId}Change fields on a deal. Send only the fields that change.

Required permissions

deals:write

Success:200

Usage

cURL
curl -X PATCH \
  'http://localhost:3000/api/public/v1/deals/8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3' \
  -H "Authorization: Bearer ast_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{
  "title": "Kitchen remodel, phase 2",
  "deal_type": "residential",
  "stage": "negotiating",
  "value": 4800,
  "priority": "high",
  "customer_id": "8e2f4a6b-1d3c-4b7e-9a0f-5c6d8e2b1a93",
  "assigned_to": "3a7d1e5c-9f4b-4c2a-8e6d-1b0f7a3c5d24",
  "expected_close_date": "2026-11-30",
  "lead_source": "Referral",
  "notes": "Approved the phase 2 scope.",
  "contact_name": "Dana Ruiz",
  "contact_email": "dana@example.com",
  "contact_phone": "+15555550100",
  "site_name": "Main house",
  "service_address_line_1": "125 Oak Street",
  "service_address_line_2": null,
  "service_city": "Springfield",
  "service_state": "IL",
  "service_postal_code": "62704",
  "metadata": {}
}'
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.deals.update({ dealId: '8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3' }, {
  "title": "Kitchen remodel, phase 2",
  "deal_type": "residential",
  "stage": "negotiating",
  "value": 4800,
  "priority": "high",
  "customer_id": "8e2f4a6b-1d3c-4b7e-9a0f-5c6d8e2b1a93",
  "assigned_to": "3a7d1e5c-9f4b-4c2a-8e6d-1b0f7a3c5d24",
  "expected_close_date": "2026-11-30",
  "lead_source": "Referral",
  "notes": "Approved the phase 2 scope.",
  "contact_name": "Dana Ruiz",
  "contact_email": "dana@example.com",
  "contact_phone": "+15555550100",
  "site_name": "Main house",
  "service_address_line_1": "125 Oak Street",
  "service_address_line_2": null,
  "service_city": "Springfield",
  "service_state": "IL",
  "service_postal_code": "62704",
  "metadata": {}
});

Request body

Fields to change on a deal. Send only what changes. Moving to another column also works here, but POST /move is the direct way.

DealUpdateRequest
{
  "title": "Kitchen remodel, phase 2",
  "deal_type": "residential",
  "stage": "negotiating",
  "value": 4800,
  "priority": "high",
  "customer_id": "8e2f4a6b-1d3c-4b7e-9a0f-5c6d8e2b1a93",
  "assigned_to": "3a7d1e5c-9f4b-4c2a-8e6d-1b0f7a3c5d24",
  "expected_close_date": "2026-11-30",
  "lead_source": "Referral",
  "notes": "Approved the phase 2 scope.",
  "contact_name": "Dana Ruiz",
  "contact_email": "dana@example.com",
  "contact_phone": "+15555550100",
  "site_name": "Main house",
  "service_address_line_1": "125 Oak Street",
  "service_address_line_2": null,
  "service_city": "Springfield",
  "service_state": "IL",
  "service_postal_code": "62704",
  "metadata": {}
}
View request fields
DealUpdateRequest fields
FieldTypeRequiredDescription
titlestringNoDeal name, up to 200 characters.
deal_type"commercial" | "residential"NoBusiness or home deal.
stagestringNoStage id to move the deal to.
valuenumberNoExpected value in dollars.
priority"low" | "medium" | "high" | "urgent"NoPriority.
customer_iduuid | nullNoCustomer in this company to link, or null to unlink.
assigned_touuid | nullNoTeam member who owns the deal, or null.
expected_close_datedate | nullNoExpected close date (YYYY-MM-DD).
lead_sourcestring | nullNoWhere the deal came from.
notesstring | nullNoNotes about the deal.
contact_namestring | nullNoContact name.
contact_emailstring | nullNoContact email.
contact_phonestring | nullNoContact phone.
site_namestring | nullNoJob site name.
service_address_line_1string | nullNoJob site street address, line 1.
service_address_line_2string | nullNoJob site street address, line 2.
service_citystring | nullNoJob site city.
service_statestring | nullNoJob site state.
service_postal_codestring | nullNoJob site postal code.
metadataobjectNoExtra deal details. Replaces the stored object when sent.

Response body

DealResponse
{
  "data": {
    "id": "5b0e8f2a-7c1d-4a6e-9f3b-2d8c4e1a6b70",
    "pipeline_id": "0c3d9e71-8b2a-4f5e-a6d4-7e1b9c0f3a52",
    "customer_id": "8e2f4a6b-1d3c-4b7e-9a0f-5c6d8e2b1a93",
    "title": "Kitchen remodel",
    "deal_type": "residential",
    "stage": "estimate_sent",
    "value": 4200,
    "priority": "medium",
    "assigned_to": "3a7d1e5c-9f4b-4c2a-8e6d-1b0f7a3c5d24",
    "expected_close_date": "2026-11-15",
    "lead_source": "Referral",
    "notes": "Wants the quote before Friday.",
    "contact_name": "Dana Ruiz",
    "contact_email": "dana@example.com",
    "contact_phone": "+15555550100",
    "site_name": "Main house",
    "service_address_line_1": "125 Oak Street",
    "service_address_line_2": null,
    "service_city": "Springfield",
    "service_state": "IL",
    "service_postal_code": "62704",
    "won_date": null,
    "lost_date": null,
    "lost_reason": null,
    "metadata": {},
    "stage_entered_at": "2026-10-20T14:30:00.000Z",
    "created_at": "2026-10-18T09:00:00.000Z",
    "updated_at": "2026-10-20T14:30:00.000Z"
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 404 Not found · 429 Rate limited · 413 Request too large · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Invalid request payload.

Usually happens when:
The request body is missing required fields or includes invalid field values.
What to do:
Validate the payload against the generated request schema and fix any `issues` entries before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Creating and changing records needs the Premium plan or above.

Usually happens when:
The company plan includes reading through the API but not writing, or the key lacks the write scope.
What to do:
Upgrade to Premium in Subscription settings, or create a key with the needed write scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

404 Not found

The deal or pipeline was not found in this company.

Usually happens when:
The id belongs to another company, was deleted, or is not a valid id.
What to do:
Check the id with GET /api/public/v1/deals or GET /api/public/v1/pipelines.
Error response
{
  "error": "We couldn't find that customer. Check its ID and try again."
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle mutation throughput and avoid blind retries from queues or worker pools.
Retry guidance:
Honor the `Retry-After` header and replay writes only after the relevant limit window resets.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

413 Request too large

The JSON request body exceeds this route’s byte limit.

Usually happens when:
The request body is larger than this write route accepts.
What to do:
Reduce the request body and try again.
Error response
{
  "error": "The request is too large. Send a smaller JSON body and try again."
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this write.
What to do:
Check API availability, then retry with the same idempotency key when applicable.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while validating or persisting the mutation.
What to do:
Capture the response payload and contract version, then retry once before escalating.
Error response
{
  "error": "We couldn't load customers. Try again."
}
POST
/api/public/v1/deals/{dealId}/moveMove a deal to another column of its pipeline. Moving to the column it is already in changes nothing.

Required permissions

deals:write

Success:201

Usage

cURL
curl -X POST \
  'http://localhost:3000/api/public/v1/deals/8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3/move' \
  -H "Authorization: Bearer ast_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{
  "stage_id": "negotiating",
  "lost_reason": "Went with another company"
}'
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.deals.move({ dealId: '8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3' }, {
  "stage_id": "negotiating",
  "lost_reason": "Went with another company"
});

Request body

The column to move a deal into.

DealMoveRequest
{
  "stage_id": "negotiating",
  "lost_reason": "Went with another company"
}
View request fields
DealMoveRequest fields
FieldTypeRequiredDescription
stage_idstringYesStage id from the deal’s pipeline (GET /api/public/v1/pipelines).
lost_reasonstringNoWhy the deal was lost. Only kept when the column is lost.

Response body

DealResponse
{
  "data": {
    "id": "5b0e8f2a-7c1d-4a6e-9f3b-2d8c4e1a6b70",
    "pipeline_id": "0c3d9e71-8b2a-4f5e-a6d4-7e1b9c0f3a52",
    "customer_id": "8e2f4a6b-1d3c-4b7e-9a0f-5c6d8e2b1a93",
    "title": "Kitchen remodel",
    "deal_type": "residential",
    "stage": "estimate_sent",
    "value": 4200,
    "priority": "medium",
    "assigned_to": "3a7d1e5c-9f4b-4c2a-8e6d-1b0f7a3c5d24",
    "expected_close_date": "2026-11-15",
    "lead_source": "Referral",
    "notes": "Wants the quote before Friday.",
    "contact_name": "Dana Ruiz",
    "contact_email": "dana@example.com",
    "contact_phone": "+15555550100",
    "site_name": "Main house",
    "service_address_line_1": "125 Oak Street",
    "service_address_line_2": null,
    "service_city": "Springfield",
    "service_state": "IL",
    "service_postal_code": "62704",
    "won_date": null,
    "lost_date": null,
    "lost_reason": null,
    "metadata": {},
    "stage_entered_at": "2026-10-20T14:30:00.000Z",
    "created_at": "2026-10-18T09:00:00.000Z",
    "updated_at": "2026-10-20T14:30:00.000Z"
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 404 Not found · 429 Rate limited · 413 Request too large · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Invalid request payload.

Usually happens when:
The request body is missing required fields or includes invalid field values.
What to do:
Validate the payload against the generated request schema and fix any `issues` entries before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Creating and changing records needs the Premium plan or above.

Usually happens when:
The company plan includes reading through the API but not writing, or the key lacks the write scope.
What to do:
Upgrade to Premium in Subscription settings, or create a key with the needed write scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

404 Not found

The deal or pipeline was not found in this company.

Usually happens when:
The id belongs to another company, was deleted, or is not a valid id.
What to do:
Check the id with GET /api/public/v1/deals or GET /api/public/v1/pipelines.
Error response
{
  "error": "We couldn't find that customer. Check its ID and try again."
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle mutation throughput and avoid blind retries from queues or worker pools.
Retry guidance:
Honor the `Retry-After` header and replay writes only after the relevant limit window resets.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

413 Request too large

The JSON request body exceeds this route’s byte limit.

Usually happens when:
The request body is larger than this write route accepts.
What to do:
Reduce the request body and try again.
Error response
{
  "error": "The request is too large. Send a smaller JSON body and try again."
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this write.
What to do:
Check API availability, then retry with the same idempotency key when applicable.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while validating or persisting the mutation.
What to do:
Capture the response payload and contract version, then retry once before escalating.
Error response
{
  "error": "We couldn't load customers. Try again."
}
GET
/api/public/v1/pipelinesList the company’s sales pipelines, default first, with their columns in order.

Required permissions

deals:read

Success:200

Usage

cURL
curl -X GET \
  'http://localhost:3000/api/public/v1/pipelines' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.pipelines.list();

Response body

PipelineListResponse
{
  "data": [
    {
      "id": "0c3d9e71-8b2a-4f5e-a6d4-7e1b9c0f3a52",
      "name": "Residential sales",
      "is_default": true,
      "stages": [
        {
          "id": "estimate_sent",
          "label": "Estimate sent",
          "color": "#f59e0b",
          "kind": "open",
          "win_probability": 65,
          "position": 3
        },
        {
          "id": "won",
          "label": "Won",
          "color": "#22c55e",
          "kind": "won",
          "win_probability": 100,
          "position": 1
        }
      ]
    }
  ]
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 429 Rate limited · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Malformed query or request payload.

Usually happens when:
The request sends malformed query/body data.
What to do:
Validate request fields against the contract examples before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Missing plan access, capability, IP access, or scopes for this request.

Usually happens when:
The key is valid but the company’s plan does not include this (reads need a paid plan, writes need Premium or above), API access is suspended, the key IP allowlist does not match, or the key lacks the required scope.
What to do:
Upgrade the plan for writes, adjust the key IP allowlist, or create a key with the required scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle request volume and avoid immediate fan-out retries from parallel workers.
Retry guidance:
Honor the `Retry-After` header and use exponential backoff for repeated limits.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this request.
What to do:
Check API availability, then retry with backoff.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while reading or serializing the resource.
What to do:
Log the contract version and response body, then retry once before escalating to Asteri support.
Error response
{
  "error": "We couldn't load customers. Try again."
}
View deals schemas and field examples

Deal

A sales opportunity on one of the company’s pipelines, with its column, value, owner and customer.

Deal fields
FieldTypeRequiredDescription
iduuidYesDeal id.
pipeline_iduuidYesPipeline the deal sits on.
customer_iduuid | nullYesLinked customer, or null when the deal has none.
titlestringYesDeal name.
deal_type"commercial" | "residential"YesWhether the deal is for a business or a home.
stagestringYesStage id: the column key from GET /api/public/v1/pipelines.
valuenumberYesExpected deal value in dollars.
priority"low" | "medium" | "high" | "urgent"YesHow urgent the deal is.
assigned_touuid | nullYesTeam member who owns the deal, or null.
expected_close_datedate | nullYesDate the deal is expected to close (YYYY-MM-DD).
lead_sourcestring | nullYesWhere the deal came from.
notesstring | nullYesNotes about the deal.
contact_namestring | nullYesName of the contact for this deal.
contact_emailstring | nullYesEmail of the contact for this deal.
contact_phonestring | nullYesPhone of the contact for this deal.
site_namestring | nullYesName of the job site.
service_address_line_1string | nullYesJob site street address, line 1.
service_address_line_2string | nullYesJob site street address, line 2.
service_citystring | nullYesJob site city.
service_statestring | nullYesJob site state.
service_postal_codestring | nullYesJob site postal code.
won_datedatetime | nullYesWhen the deal was won, or null.
lost_datedatetime | nullYesWhen the deal was lost, or null.
lost_reasonstring | nullYesWhy the deal was lost, when it was.
metadataobjectYesExtra deal details the app stores with the deal.
stage_entered_atdatetimeYesWhen the deal entered its current stage.
created_atdatetimeYesWhen the deal was created.
updated_atdatetimeYesWhen the deal last changed.
Example Deal
{
  "id": "5b0e8f2a-7c1d-4a6e-9f3b-2d8c4e1a6b70",
  "pipeline_id": "0c3d9e71-8b2a-4f5e-a6d4-7e1b9c0f3a52",
  "customer_id": "8e2f4a6b-1d3c-4b7e-9a0f-5c6d8e2b1a93",
  "title": "Kitchen remodel",
  "deal_type": "residential",
  "stage": "estimate_sent",
  "value": 4200,
  "priority": "medium",
  "assigned_to": "3a7d1e5c-9f4b-4c2a-8e6d-1b0f7a3c5d24",
  "expected_close_date": "2026-11-15",
  "lead_source": "Referral",
  "notes": "Wants the quote before Friday.",
  "contact_name": "Dana Ruiz",
  "contact_email": "dana@example.com",
  "contact_phone": "+15555550100",
  "site_name": "Main house",
  "service_address_line_1": "125 Oak Street",
  "service_address_line_2": null,
  "service_city": "Springfield",
  "service_state": "IL",
  "service_postal_code": "62704",
  "won_date": null,
  "lost_date": null,
  "lost_reason": null,
  "metadata": {},
  "stage_entered_at": "2026-10-20T14:30:00.000Z",
  "created_at": "2026-10-18T09:00:00.000Z",
  "updated_at": "2026-10-20T14:30:00.000Z"
}

PipelineStage

One column of a pipeline, in order.

PipelineStage fields
FieldTypeRequiredDescription
idstringYesStage id. Send this as stage_id when moving a deal, and find it in a deal’s stage.
labelstringYesColumn name shown in the app.
colorstringYesColumn colour as a hex code.
kind"open" | "won" | "lost"YesWhether deals in this column are open, won or lost.
win_probabilityintegerYesChance (0 to 100) that an open deal here is won.
positionintegerYesPlace in the pipeline, starting at 0.
Example PipelineStage
{
  "id": "estimate_sent",
  "label": "Estimate sent",
  "color": "#f59e0b",
  "kind": "open",
  "win_probability": 65,
  "position": 3
}

Pipeline

A sales pipeline with its columns in order.

Pipeline fields
FieldTypeRequiredDescription
iduuidYesPipeline id.
namestringYesPipeline name.
is_defaultbooleanYesWhether this is the company’s default pipeline.
stagesPipelineStage[]YesColumns in order. Won and lost are always present.
Example Pipeline
{
  "id": "0c3d9e71-8b2a-4f5e-a6d4-7e1b9c0f3a52",
  "name": "Residential sales",
  "is_default": true,
  "stages": [
    {
      "id": "estimate_sent",
      "label": "Estimate sent",
      "color": "#f59e0b",
      "kind": "open",
      "win_probability": 65,
      "position": 3
    },
    {
      "id": "won",
      "label": "Won",
      "color": "#22c55e",
      "kind": "won",
      "win_probability": 100,
      "position": 1
    }
  ]
}

DealCreateRequest

Fields for a new deal. pipeline_id and title are required; the rest are optional.

DealCreateRequest fields
FieldTypeRequiredDescription
pipeline_iduuidYesPipeline to put the deal on. Ids come from GET /api/public/v1/pipelines.
titlestringYesDeal name, up to 200 characters.
deal_type"commercial" | "residential"NoBusiness or home deal. Defaults to commercial.
stagestringNoStage id to start in. Defaults to the first open column of the pipeline.
valuenumberNoExpected value in dollars. Defaults to 0.
priority"low" | "medium" | "high" | "urgent"NoPriority. Defaults to medium.
customer_iduuid | nullNoCustomer in this company to link.
assigned_touuid | nullNoTeam member in this company who owns the deal.
expected_close_datedate | nullNoExpected close date (YYYY-MM-DD).
lead_sourcestring | nullNoWhere the deal came from, up to 100 characters.
notesstring | nullNoNotes about the deal.
contact_namestring | nullNoContact name.
contact_emailstring | nullNoContact email.
contact_phonestring | nullNoContact phone.
site_namestring | nullNoJob site name.
service_address_line_1string | nullNoJob site street address, line 1.
service_address_line_2string | nullNoJob site street address, line 2.
service_citystring | nullNoJob site city.
service_statestring | nullNoJob site state.
service_postal_codestring | nullNoJob site postal code.
metadataobjectNoExtra deal details, such as the deal profile.
Example DealCreateRequest
{
  "pipeline_id": "0c3d9e71-8b2a-4f5e-a6d4-7e1b9c0f3a52",
  "title": "Kitchen remodel",
  "deal_type": "residential",
  "stage": "new_lead",
  "value": 4200,
  "priority": "medium",
  "customer_id": "8e2f4a6b-1d3c-4b7e-9a0f-5c6d8e2b1a93",
  "assigned_to": "3a7d1e5c-9f4b-4c2a-8e6d-1b0f7a3c5d24",
  "expected_close_date": "2026-11-15",
  "lead_source": "Referral",
  "notes": "Wants the quote before Friday.",
  "contact_name": "Dana Ruiz",
  "contact_email": "dana@example.com",
  "contact_phone": "+15555550100",
  "site_name": "Main house",
  "service_address_line_1": "125 Oak Street",
  "service_address_line_2": null,
  "service_city": "Springfield",
  "service_state": "IL",
  "service_postal_code": "62704",
  "metadata": {}
}

DealUpdateRequest

Fields to change on a deal. Send only what changes. Moving to another column also works here, but POST /move is the direct way.

DealUpdateRequest fields
FieldTypeRequiredDescription
titlestringNoDeal name, up to 200 characters.
deal_type"commercial" | "residential"NoBusiness or home deal.
stagestringNoStage id to move the deal to.
valuenumberNoExpected value in dollars.
priority"low" | "medium" | "high" | "urgent"NoPriority.
customer_iduuid | nullNoCustomer in this company to link, or null to unlink.
assigned_touuid | nullNoTeam member who owns the deal, or null.
expected_close_datedate | nullNoExpected close date (YYYY-MM-DD).
lead_sourcestring | nullNoWhere the deal came from.
notesstring | nullNoNotes about the deal.
contact_namestring | nullNoContact name.
contact_emailstring | nullNoContact email.
contact_phonestring | nullNoContact phone.
site_namestring | nullNoJob site name.
service_address_line_1string | nullNoJob site street address, line 1.
service_address_line_2string | nullNoJob site street address, line 2.
service_citystring | nullNoJob site city.
service_statestring | nullNoJob site state.
service_postal_codestring | nullNoJob site postal code.
metadataobjectNoExtra deal details. Replaces the stored object when sent.
Example DealUpdateRequest
{
  "title": "Kitchen remodel, phase 2",
  "deal_type": "residential",
  "stage": "negotiating",
  "value": 4800,
  "priority": "high",
  "customer_id": "8e2f4a6b-1d3c-4b7e-9a0f-5c6d8e2b1a93",
  "assigned_to": "3a7d1e5c-9f4b-4c2a-8e6d-1b0f7a3c5d24",
  "expected_close_date": "2026-11-30",
  "lead_source": "Referral",
  "notes": "Approved the phase 2 scope.",
  "contact_name": "Dana Ruiz",
  "contact_email": "dana@example.com",
  "contact_phone": "+15555550100",
  "site_name": "Main house",
  "service_address_line_1": "125 Oak Street",
  "service_address_line_2": null,
  "service_city": "Springfield",
  "service_state": "IL",
  "service_postal_code": "62704",
  "metadata": {}
}

DealMoveRequest

The column to move a deal into.

DealMoveRequest fields
FieldTypeRequiredDescription
stage_idstringYesStage id from the deal’s pipeline (GET /api/public/v1/pipelines).
lost_reasonstringNoWhy the deal was lost. Only kept when the column is lost.
Example DealMoveRequest
{
  "stage_id": "negotiating",
  "lost_reason": "Went with another company"
}

Availability

Bookable appointment times for your services, worked out with the same rules as online booking.

/api/public/v1/availability
GET
/api/public/v1/availabilityList bookable slots for up to 14 days. Send start_date (YYYY-MM-DD) and service_ids (comma separated) or service_id. Optional: days (default 7), team_member_id and postal_code.

Required permissions

availability:read

Success:200

Usage

cURL
curl -X GET \
  'http://localhost:3000/api/public/v1/availability' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.availability.list();

Response body

AvailabilityListResponse
{
  "data": [
    {
      "date": "2026-11-02",
      "timezone": "America/New_York",
      "service_ids": [
        "5b7f9d8e-2c1a-4f3b-9e6d-7a8b9c0d1e2f"
      ],
      "total_duration_minutes": 60,
      "slots": [
        {
          "start_time": "2026-11-02T15:00:00.000Z",
          "end_time": "2026-11-02T16:00:00.000Z",
          "duration_minutes": 60
        }
      ]
    }
  ]
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 404 Not found · 412 Time zone needed · 429 Rate limited · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Malformed query or request payload.

Usually happens when:
The request sends malformed query/body data.
What to do:
Validate request fields against the contract examples before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Missing plan access, capability, IP access, or scopes for this request.

Usually happens when:
The key is valid but the company’s plan does not include this (reads need a paid plan, writes need Premium or above), API access is suspended, the key IP allowlist does not match, or the key lacks the required scope.
What to do:
Upgrade the plan for writes, adjust the key IP allowlist, or create a key with the required scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

404 Not found

Online booking is off, or a service is missing or inactive.

Usually happens when:
Online booking is turned off, or a service ID belongs to another company or is inactive.
What to do:
Turn on online booking in settings, or use a service ID from the services list.
Error response
{
  "error": "We couldn't find that customer. Check its ID and try again."
}

412 Time zone needed

The business has no valid time zone.

Usually happens when:
The business time zone in its settings is missing or not recognized.
What to do:
Set the business time zone in company settings, then retry.
Error response
{
  "error": "Configure or manually resolve sales tax before publishing."
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle request volume and avoid immediate fan-out retries from parallel workers.
Retry guidance:
Honor the `Retry-After` header and use exponential backoff for repeated limits.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this request.
What to do:
Check API availability, then retry with backoff.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while reading or serializing the resource.
What to do:
Log the contract version and response body, then retry once before escalating to Asteri support.
Error response
{
  "error": "We couldn't load customers. Try again."
}
View availability schemas and field examples

AvailabilitySlot

One appointment time a customer can book.

AvailabilitySlot fields
FieldTypeRequiredDescription
start_timedatetimeYesStart of the slot in UTC (ISO 8601). Send it back as scheduled_start when booking.
end_timedatetimeYesEnd of the slot in UTC (ISO 8601). Send it back as scheduled_end when booking.
duration_minutesintegerYesLength of the slot in minutes, from the services chosen.
Example AvailabilitySlot
{
  "start_time": "2026-11-02T15:00:00.000Z",
  "end_time": "2026-11-02T16:00:00.000Z",
  "duration_minutes": 60
}

AvailabilityDay

Bookable slots for one calendar day in the business time zone.

AvailabilityDay fields
FieldTypeRequiredDescription
datedateYesThe calendar day in the business time zone (YYYY-MM-DD).
timezonestringYesThe business time zone (IANA name) the day is worked out in.
service_idsuuid[]YesServices the slots are for.
total_duration_minutesintegerYesTotal time the chosen services take, in minutes.
slotsAvailabilitySlot[]YesBookable slots for the day, earliest first. Empty when nothing is free.
messagestringNoWhy there are no slots, when the business is closed that day.
unavailable_reasonstringNoWhy this day has no slots, when the day cannot be booked (for example, it is past the booking window).
Example AvailabilityDay
{
  "date": "2026-11-02",
  "timezone": "America/New_York",
  "service_ids": [
    "5b7f9d8e-2c1a-4f3b-9e6d-7a8b9c0d1e2f"
  ],
  "total_duration_minutes": 60,
  "slots": [
    {
      "start_time": "2026-11-02T15:00:00.000Z",
      "end_time": "2026-11-02T16:00:00.000Z",
      "duration_minutes": 60
    }
  ]
}

Bookings

Book an appointment slot for a customer with the same checks as online booking.

/api/public/v1/bookings
POST
/api/public/v1/bookingsBook a slot for a customer. Send an Idempotency-Key header (8 to 128 characters). A retry with the same key and body returns the same booking.

Required permissions

bookings:write

Success:201

Usage

cURL
curl -X POST \
  'http://localhost:3000/api/public/v1/bookings' \
  -H "Authorization: Bearer ast_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{
  "scheduled_start": "2026-11-02T15:00:00.000Z",
  "scheduled_end": "2026-11-02T16:00:00.000Z",
  "service_ids": [
    "5b7f9d8e-2c1a-4f3b-9e6d-7a8b9c0d1e2f"
  ],
  "customer_id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3"
}'
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.bookings.create({
  "scheduled_start": "2026-11-02T15:00:00.000Z",
  "scheduled_end": "2026-11-02T16:00:00.000Z",
  "service_ids": [
    "5b7f9d8e-2c1a-4f3b-9e6d-7a8b9c0d1e2f"
  ],
  "customer_id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3"
});

Request body

Books the services at one slot. Take scheduled_start and scheduled_end from a slot in the availability response.

BookingCreateRequest
{
  "scheduled_start": "2026-11-02T15:00:00.000Z",
  "scheduled_end": "2026-11-02T16:00:00.000Z",
  "service_ids": [
    "5b7f9d8e-2c1a-4f3b-9e6d-7a8b9c0d1e2f"
  ],
  "customer_id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3"
}
View request fields
BookingCreateRequest fields
FieldTypeRequiredDescription
scheduled_startdatetimeYesStart of the booking in ISO 8601 with a time zone. Use a slot start_time.
scheduled_enddatetimeYesEnd of the booking. Use the matching slot end_time. Must be after the start.
service_idsuuid[]NoServices to book, up to the limit in your booking settings.
customer_iduuidNoAn existing customer. Leave out to create one from the contact fields.
customer_first_namestringNoFirst name for a new customer.
customer_last_namestringNoLast name for a new customer.
customer_emailstringNoEmail address for a new customer.
customer_phonestringNoPhone number for a new customer.
service_address_iduuidNoService address on the customer. Checked against the service area.
assigned_team_member_idsuuid[]NoTeam members to assign. Leave out to let scheduling choose.
descriptionstring | nullNoNotes for the job.

Response body

BookingResponse
{
  "data": {
    "id": "0d6c2f7e-1a2b-4c3d-8e9f-a0b1c2d3e4f5",
    "customer_id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
    "status": "scheduled",
    "scheduled_start": "2026-11-02T15:00:00.000Z",
    "scheduled_end": "2026-11-02T16:00:00.000Z",
    "description": null,
    "assigned_team_member_ids": [],
    "service_ids": [
      "5b7f9d8e-2c1a-4f3b-9e6d-7a8b9c0d1e2f"
    ],
    "created_at": "2026-10-23T18:04:00.000Z"
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 404 Not found · 409 Time not available · 429 Rate limited · 413 Request too large · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Invalid request payload.

Usually happens when:
The request body is missing required fields or includes invalid field values.
What to do:
Validate the payload against the generated request schema and fix any `issues` entries before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Creating and changing records needs the Premium plan or above.

Usually happens when:
The company plan includes reading through the API but not writing, or the key lacks the write scope.
What to do:
Upgrade to Premium in Subscription settings, or create a key with the needed write scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

404 Not found

The customer, address, service or team member is not in this company.

Usually happens when:
An ID in the request belongs to another company, was removed, or is inactive.
What to do:
Check the customer, address, service and team member IDs, then send the request again.
Error response
{
  "error": "We couldn't find that customer. Check its ID and try again."
}

409 Time not available

The slot is taken, outside business hours, or the day is full.

Usually happens when:
Another booking took the time, the interval is outside business hours, or the same-day limit is reached.
What to do:
Fetch availability again and book one of the slots it returns.
Error response
{
  "error": "A customer already uses this email. Use a different email or open the existing customer.",
  "field": "email"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle mutation throughput and avoid blind retries from queues or worker pools.
Retry guidance:
Honor the `Retry-After` header and replay writes only after the relevant limit window resets.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

413 Request too large

The JSON request body exceeds this route’s byte limit.

Usually happens when:
The request body is larger than this write route accepts.
What to do:
Reduce the request body and try again.
Error response
{
  "error": "The request is too large. Send a smaller JSON body and try again."
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this write.
What to do:
Check API availability, then retry with the same idempotency key when applicable.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while validating or persisting the mutation.
What to do:
Capture the response payload and contract version, then retry once before escalating.
Error response
{
  "error": "We couldn't load customers. Try again."
}
View bookings schemas and field examples

Booking

An appointment booked through the public API.

Booking fields
FieldTypeRequiredDescription
iduuidYesAppointment identifier.
customer_iduuidYesCustomer the appointment is for.
statusstringYesAppointment status when booked, such as scheduled.
scheduled_startdatetimeYesStart of the appointment in UTC (ISO 8601).
scheduled_enddatetime | nullYesEnd of the appointment in UTC (ISO 8601).
descriptionstring | nullYesNotes for the job.
assigned_team_member_idsuuid[]YesTeam members assigned to the appointment.
service_idsuuid[]YesServices on the appointment.
created_atdatetime | nullYesWhen the booking was created (ISO 8601).
same_daybooleanNoTrue when the booking is for today. Present on bookings made after same-day rules.
same_day_feenumber | nullNoSame-day rush fee added to the booking, when one applies.
awaiting_confirmationbooleanNoTrue when a same-day booking waits for the business to confirm it.
Example Booking
{
  "id": "0d6c2f7e-1a2b-4c3d-8e9f-a0b1c2d3e4f5",
  "customer_id": "8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3",
  "status": "scheduled",
  "scheduled_start": "2026-11-02T15:00:00.000Z",
  "scheduled_end": "2026-11-02T16:00:00.000Z",
  "description": null,
  "assigned_team_member_ids": [],
  "service_ids": [
    "5b7f9d8e-2c1a-4f3b-9e6d-7a8b9c0d1e2f"
  ],
  "created_at": "2026-10-23T18:04:00.000Z"
}

Tasks

Work items with a due day, an optional reminder time, one assignee and a customer.

/api/public/v1/tasks
GET
/api/public/v1/tasksList tasks. Filter with status (open or done), assignee_team_member_id, customer_id, due_before and due_after (strict days). Page with limit and cursor.

Required permissions

tasks:read

Success:200

Usage

cURL
curl -X GET \
  'http://localhost:3000/api/public/v1/tasks' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.tasks.list();

Response body

TaskListResponse
{
  "data": [
    {
      "id": "9b1d4c2e-1a3f-4e5d-8c6b-2a7f0e9d1c33",
      "title": "Call the customer back",
      "description": null,
      "priority": "Medium",
      "status": "open",
      "due_date": "2026-06-11",
      "due_at": null,
      "reminder_at": null,
      "customer_id": null,
      "assignee_team_member_id": null,
      "assignee_team_member_ids": [],
      "completed_at": null,
      "created_at": "2026-06-11T14:00:00.000Z",
      "updated_at": "2026-06-11T14:00:00.000Z"
    }
  ],
  "pagination": {
    "count": 1,
    "limit": 25,
    "next_cursor": null
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 429 Rate limited · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Malformed query or request payload.

Usually happens when:
The request sends malformed query/body data.
What to do:
Validate request fields against the contract examples before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Missing plan access, capability, IP access, or scopes for this request.

Usually happens when:
The key is valid but the company’s plan does not include this (reads need a paid plan, writes need Premium or above), API access is suspended, the key IP allowlist does not match, or the key lacks the required scope.
What to do:
Upgrade the plan for writes, adjust the key IP allowlist, or create a key with the required scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle request volume and avoid immediate fan-out retries from parallel workers.
Retry guidance:
Honor the `Retry-After` header and use exponential backoff for repeated limits.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this request.
What to do:
Check API availability, then retry with backoff.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while reading or serializing the resource.
What to do:
Log the contract version and response body, then retry once before escalating to Asteri support.
Error response
{
  "error": "We couldn't load customers. Try again."
}
POST
/api/public/v1/tasksCreate a task. Send an Idempotency-Key header; a retry with the same key and body returns the first task.

Required permissions

tasks:write

Success:201

Usage

cURL
curl -X POST \
  'http://localhost:3000/api/public/v1/tasks' \
  -H "Authorization: Bearer ast_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{
  "title": "Call the customer back",
  "description": null,
  "priority": "Medium",
  "due_date": "2026-06-12",
  "due_at": "2026-06-12T15:00:00-04:00",
  "reminder_at": "2026-06-12T09:30:00-04:00",
  "assignee_team_member_id": null,
  "customer_id": null
}'
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.tasks.create({
  "title": "Call the customer back",
  "description": null,
  "priority": "Medium",
  "due_date": "2026-06-12",
  "due_at": "2026-06-12T15:00:00-04:00",
  "reminder_at": "2026-06-12T09:30:00-04:00",
  "assignee_team_member_id": null,
  "customer_id": null
});

Request body

Fields for a new task. Send an Idempotency-Key header with every new task.

CreateTaskRequest
{
  "title": "Call the customer back",
  "description": null,
  "priority": "Medium",
  "due_date": "2026-06-12",
  "due_at": "2026-06-12T15:00:00-04:00",
  "reminder_at": "2026-06-12T09:30:00-04:00",
  "assignee_team_member_id": null,
  "customer_id": null
}
View request fields
CreateTaskRequest fields
FieldTypeRequiredDescription
titlestringYesWhat needs doing (1 to 500 characters).
descriptionstring | nullNoLonger notes (up to 5,000 characters).
priority"Low" | "Medium" | "High" | "Critical"NoHow urgent the task is. Defaults to Medium.
due_datedateNoThe day the task is due (YYYY-MM-DD, company time zone). Defaults to today in the company time zone.
due_atdatetime | nullNoThe time the task is due, with its offset. When sent, its company day becomes the due day.
reminder_atdatetime | nullNoWhen the reminder goes out. At least a minute ahead and up to a year.
assignee_team_member_iduuid | nullNoAn active team member of the company.
customer_iduuid | nullNoA customer of the company.

Response body

CreateTaskResponse
{
  "data": {
    "id": "9b1d4c2e-1a3f-4e5d-8c6b-2a7f0e9d1c33",
    "title": "Call the customer back",
    "description": null,
    "priority": "Medium",
    "status": "open",
    "due_date": "2026-06-11",
    "due_at": null,
    "reminder_at": null,
    "customer_id": null,
    "assignee_team_member_id": null,
    "assignee_team_member_ids": [],
    "completed_at": null,
    "created_at": "2026-06-11T14:00:00.000Z",
    "updated_at": "2026-06-11T14:00:00.000Z"
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 409 Conflict · 429 Rate limited · 413 Request too large · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Invalid request payload.

Usually happens when:
The request body is missing required fields or includes invalid field values.
What to do:
Validate the payload against the generated request schema and fix any `issues` entries before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Creating and changing records needs the Premium plan or above.

Usually happens when:
The company plan includes reading through the API but not writing, or the key lacks the write scope.
What to do:
Upgrade to Premium in Subscription settings, or create a key with the needed write scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

409 Conflict

The Idempotency-Key was already used for a different task.

Usually happens when:
A retry sends the same Idempotency-Key with a different body.
What to do:
Send a new Idempotency-Key for a new task. Retrying the same body returns the first task.
Error response
{
  "error": "A customer already uses this email. Use a different email or open the existing customer.",
  "field": "email"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle mutation throughput and avoid blind retries from queues or worker pools.
Retry guidance:
Honor the `Retry-After` header and replay writes only after the relevant limit window resets.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

413 Request too large

The JSON request body exceeds this route’s byte limit.

Usually happens when:
The request body is larger than this write route accepts.
What to do:
Reduce the request body and try again.
Error response
{
  "error": "The request is too large. Send a smaller JSON body and try again."
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this write.
What to do:
Check API availability, then retry with the same idempotency key when applicable.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while validating or persisting the mutation.
What to do:
Capture the response payload and contract version, then retry once before escalating.
Error response
{
  "error": "We couldn't load customers. Try again."
}
GET
/api/public/v1/tasks/{taskId}Read one task in the company.

Required permissions

tasks:read

Success:200

Usage

cURL
curl -X GET \
  'http://localhost:3000/api/public/v1/tasks/8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.tasks.get({ taskId: '8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3' });

Response body

TaskResponse
{
  "data": {
    "id": "9b1d4c2e-1a3f-4e5d-8c6b-2a7f0e9d1c33",
    "title": "Call the customer back",
    "description": null,
    "priority": "Medium",
    "status": "open",
    "due_date": "2026-06-11",
    "due_at": null,
    "reminder_at": null,
    "customer_id": null,
    "assignee_team_member_id": null,
    "assignee_team_member_ids": [],
    "completed_at": null,
    "created_at": "2026-06-11T14:00:00.000Z",
    "updated_at": "2026-06-11T14:00:00.000Z"
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 404 Not found · 429 Rate limited · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Malformed query or request payload.

Usually happens when:
The request sends malformed query/body data.
What to do:
Validate request fields against the contract examples before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Missing plan access, capability, IP access, or scopes for this request.

Usually happens when:
The key is valid but the company’s plan does not include this (reads need a paid plan, writes need Premium or above), API access is suspended, the key IP allowlist does not match, or the key lacks the required scope.
What to do:
Upgrade the plan for writes, adjust the key IP allowlist, or create a key with the required scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

404 Not found

The task is not in this company, or it does not exist.

Usually happens when:
The task id is wrong, or it belongs to another company.
What to do:
Check the task id from the list endpoint.
Error response
{
  "error": "We couldn't find that customer. Check its ID and try again."
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle request volume and avoid immediate fan-out retries from parallel workers.
Retry guidance:
Honor the `Retry-After` header and use exponential backoff for repeated limits.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this request.
What to do:
Check API availability, then retry with backoff.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while reading or serializing the resource.
What to do:
Log the contract version and response body, then retry once before escalating to Asteri support.
Error response
{
  "error": "We couldn't load customers. Try again."
}
PATCH
/api/public/v1/tasks/{taskId}Change a task: title, due day, reminder time, assignee, or done and reopened.

Required permissions

tasks:write

Success:200

Usage

cURL
curl -X PATCH \
  'http://localhost:3000/api/public/v1/tasks/8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3' \
  -H "Authorization: Bearer ast_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{
  "title": "Call back after the quote",
  "due_date": "2026-06-15",
  "due_at": "2026-06-15T15:00:00-04:00",
  "reminder_at": null,
  "assignee_team_member_id": null,
  "completed": true
}'
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.tasks.update({ taskId: '8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3' }, {
  "title": "Call back after the quote",
  "due_date": "2026-06-15",
  "due_at": "2026-06-15T15:00:00-04:00",
  "reminder_at": null,
  "assignee_team_member_id": null,
  "completed": true
});

Request body

Changes to a task. Send at least one field. Assigning replaces the assignee with that one person.

UpdateTaskRequest
{
  "title": "Call back after the quote",
  "due_date": "2026-06-15",
  "due_at": "2026-06-15T15:00:00-04:00",
  "reminder_at": null,
  "assignee_team_member_id": null,
  "completed": true
}
View request fields
UpdateTaskRequest fields
FieldTypeRequiredDescription
titlestringNoA new title (1 to 500 characters).
due_datedateNoA new due day (YYYY-MM-DD). A reminder that has not gone out moves with it.
due_atdatetime | nullNoA new due time with its offset, or null to clear it. Its company day becomes the due day.
reminder_atdatetime | nullNoA new reminder time, or null to remove the reminder.
assignee_team_member_iduuid | nullNoThe person the task is assigned to, or null to unassign.
completedbooleanNotrue marks the task done. false reopens it.

Response body

UpdateTaskResponse
{
  "data": {
    "id": "9b1d4c2e-1a3f-4e5d-8c6b-2a7f0e9d1c33",
    "title": "Call the customer back",
    "description": null,
    "priority": "Medium",
    "status": "open",
    "due_date": "2026-06-11",
    "due_at": null,
    "reminder_at": null,
    "customer_id": null,
    "assignee_team_member_id": null,
    "assignee_team_member_ids": [],
    "completed_at": null,
    "created_at": "2026-06-11T14:00:00.000Z",
    "updated_at": "2026-06-11T14:00:00.000Z"
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 404 Not found · 429 Rate limited · 413 Request too large · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Invalid request payload.

Usually happens when:
The request body is missing required fields or includes invalid field values.
What to do:
Validate the payload against the generated request schema and fix any `issues` entries before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Creating and changing records needs the Premium plan or above.

Usually happens when:
The company plan includes reading through the API but not writing, or the key lacks the write scope.
What to do:
Upgrade to Premium in Subscription settings, or create a key with the needed write scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

404 Not found

The task is not in this company, or it does not exist.

Usually happens when:
The task id is wrong, or it belongs to another company.
What to do:
Check the task id from the list endpoint.
Error response
{
  "error": "We couldn't find that customer. Check its ID and try again."
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle mutation throughput and avoid blind retries from queues or worker pools.
Retry guidance:
Honor the `Retry-After` header and replay writes only after the relevant limit window resets.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

413 Request too large

The JSON request body exceeds this route’s byte limit.

Usually happens when:
The request body is larger than this write route accepts.
What to do:
Reduce the request body and try again.
Error response
{
  "error": "The request is too large. Send a smaller JSON body and try again."
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this write.
What to do:
Check API availability, then retry with the same idempotency key when applicable.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while validating or persisting the mutation.
What to do:
Capture the response payload and contract version, then retry once before escalating.
Error response
{
  "error": "We couldn't load customers. Try again."
}
View tasks schemas and field examples

Task

A task for the team. Its status is open until it is marked done. It has a due day in the company time zone, an optional reminder time, and at most one assignee through the API.

Task fields
FieldTypeRequiredDescription
iduuidYesTask identifier.
titlestringYesWhat needs doing.
descriptionstring | nullYesLonger notes for the task.
priority"Low" | "Medium" | "High" | "Critical"YesHow urgent the task is.
status"open" | "done"Yesopen until the task is marked done. In-progress tasks show as open.
due_datedateYesThe day the task is due, in the company time zone (YYYY-MM-DD).
due_atdatetime | nullYesThe time the task is due, when one was set. Its company day is due_date. Null when the task has only a due day.
reminder_atdatetime | nullYesWhen the reminder goes out. This is the task’s time; tasks have a due day, not a due hour.
customer_iduuid | nullYesThe customer the task is for.
assignee_team_member_iduuid | nullYesThe one person the task is assigned to, or null when nobody is.
assignee_team_member_idsuuid[]YesEveryone assigned to the task. Tasks changed in the app can have several people.
completed_atdatetime | nullYesWhen the task was marked done, or null while it is open.
created_atdatetime | nullYesWhen the task was created.
updated_atdatetime | nullYesWhen the task last changed.
Example Task
{
  "id": "9b1d4c2e-1a3f-4e5d-8c6b-2a7f0e9d1c33",
  "title": "Call the customer back",
  "description": null,
  "priority": "Medium",
  "status": "open",
  "due_date": "2026-06-11",
  "due_at": null,
  "reminder_at": null,
  "customer_id": null,
  "assignee_team_member_id": null,
  "assignee_team_member_ids": [],
  "completed_at": null,
  "created_at": "2026-06-11T14:00:00.000Z",
  "updated_at": "2026-06-11T14:00:00.000Z"
}

Customer notes

Notes on a customer’s record, written the way the app writes them.

/api/public/v1/notes
GET
/api/public/v1/notesList the notes of one customer, newest first. Requires customer_id. Page with limit and cursor.

Required permissions

notes:read

Success:200

Usage

cURL
curl -X GET \
  'http://localhost:3000/api/public/v1/notes' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.notes.list();

Response body

NoteListResponse
{
  "data": [
    {
      "id": "4d7b1e3c-2a9f-4b8e-9c1d-6f0a5e2b7c10",
      "customer_id": "0c2e9f4a-3b1d-4a6e-8f7c-5d9b2e1a4f66",
      "body": "Gate code is 4471.",
      "is_important": false,
      "author_team_member_id": null,
      "created_at": "2026-06-11T14:00:00.000Z"
    }
  ],
  "pagination": {
    "count": 1,
    "limit": 25,
    "next_cursor": null
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 404 Not found · 429 Rate limited · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Malformed query or request payload.

Usually happens when:
The request sends malformed query/body data.
What to do:
Validate request fields against the contract examples before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Missing plan access, capability, IP access, or scopes for this request.

Usually happens when:
The key is valid but the company’s plan does not include this (reads need a paid plan, writes need Premium or above), API access is suspended, the key IP allowlist does not match, or the key lacks the required scope.
What to do:
Upgrade the plan for writes, adjust the key IP allowlist, or create a key with the required scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

404 Not found

The customer is not in this company, or it does not exist.

Usually happens when:
The customer_id is wrong, or it belongs to another company.
What to do:
Check the customer id from the customers endpoint.
Error response
{
  "error": "We couldn't find that customer. Check its ID and try again."
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle request volume and avoid immediate fan-out retries from parallel workers.
Retry guidance:
Honor the `Retry-After` header and use exponential backoff for repeated limits.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this request.
What to do:
Check API availability, then retry with backoff.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while reading or serializing the resource.
What to do:
Log the contract version and response body, then retry once before escalating to Asteri support.
Error response
{
  "error": "We couldn't load customers. Try again."
}
POST
/api/public/v1/notesAdd a note to a customer. Send an Idempotency-Key header; a retry with the same key and body returns the first note.

Required permissions

notes:write

Success:201

Usage

cURL
curl -X POST \
  'http://localhost:3000/api/public/v1/notes' \
  -H "Authorization: Bearer ast_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{
  "customer_id": "0c2e9f4a-3b1d-4a6e-8f7c-5d9b2e1a4f66",
  "body": "Gate code is 4471.",
  "is_important": false
}'
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.notes.create({
  "customer_id": "0c2e9f4a-3b1d-4a6e-8f7c-5d9b2e1a4f66",
  "body": "Gate code is 4471.",
  "is_important": false
});

Request body

A note for one of the company’s customers. Send an Idempotency-Key header with every new note.

CreateNoteRequest
{
  "customer_id": "0c2e9f4a-3b1d-4a6e-8f7c-5d9b2e1a4f66",
  "body": "Gate code is 4471.",
  "is_important": false
}
View request fields
CreateNoteRequest fields
FieldTypeRequiredDescription
customer_iduuidYesThe customer the note is for.
bodystringYesThe note (1 to 10,000 characters).
is_importantbooleanNoFlag the note as important. Defaults to false.

Response body

CreateNoteResponse
{
  "data": {
    "id": "4d7b1e3c-2a9f-4b8e-9c1d-6f0a5e2b7c10",
    "customer_id": "0c2e9f4a-3b1d-4a6e-8f7c-5d9b2e1a4f66",
    "body": "Gate code is 4471.",
    "is_important": false,
    "author_team_member_id": null,
    "created_at": "2026-06-11T14:00:00.000Z"
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 409 Conflict · 429 Rate limited · 413 Request too large · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Invalid request payload.

Usually happens when:
The request body is missing required fields or includes invalid field values.
What to do:
Validate the payload against the generated request schema and fix any `issues` entries before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Creating and changing records needs the Premium plan or above.

Usually happens when:
The company plan includes reading through the API but not writing, or the key lacks the write scope.
What to do:
Upgrade to Premium in Subscription settings, or create a key with the needed write scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

409 Conflict

The Idempotency-Key was already used for a different note.

Usually happens when:
A retry sends the same Idempotency-Key with a different body.
What to do:
Send a new Idempotency-Key for a new note. Retrying the same body returns the first note.
Error response
{
  "error": "A customer already uses this email. Use a different email or open the existing customer.",
  "field": "email"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle mutation throughput and avoid blind retries from queues or worker pools.
Retry guidance:
Honor the `Retry-After` header and replay writes only after the relevant limit window resets.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

413 Request too large

The JSON request body exceeds this route’s byte limit.

Usually happens when:
The request body is larger than this write route accepts.
What to do:
Reduce the request body and try again.
Error response
{
  "error": "The request is too large. Send a smaller JSON body and try again."
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this write.
What to do:
Check API availability, then retry with the same idempotency key when applicable.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while validating or persisting the mutation.
What to do:
Capture the response payload and contract version, then retry once before escalating.
Error response
{
  "error": "We couldn't load customers. Try again."
}
View customer notes schemas and field examples

Note

A note on a customer’s record. API notes do not show an author.

Note fields
FieldTypeRequiredDescription
iduuidYesNote identifier.
customer_iduuidYesThe customer the note belongs to.
bodystringYesThe note text.
is_importantbooleanYesWhether the note is flagged as important.
author_team_member_iduuid | nullYesThe team member who wrote the note in the app. Null for notes written through the API, because an API key is not a staff member.
created_atdatetime | nullYesWhen the note was added.
Example Note
{
  "id": "4d7b1e3c-2a9f-4b8e-9c1d-6f0a5e2b7c10",
  "customer_id": "0c2e9f4a-3b1d-4a6e-8f7c-5d9b2e1a4f66",
  "body": "Gate code is 4471.",
  "is_important": false,
  "author_team_member_id": null,
  "created_at": "2026-06-11T14:00:00.000Z"
}

Workflows

List workflows and enroll or remove one customer at a time.

/api/public/v1/workflows
GET
/api/public/v1/workflowsList this company’s workflows, with whether each can enroll customers.

Required permissions

workflows:read

Success:200

Usage

cURL
curl -X GET \
  'http://localhost:3000/api/public/v1/workflows' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.workflows.list();

Response body

WorkflowListResponse
{
  "data": [
    {
      "id": "1f0c3c2e-6b1a-4c7e-9d2f-3a5b7c9e1f20",
      "name": "New customer welcome",
      "description": null,
      "status": "active",
      "trigger_type": "manual",
      "enrollable": true
    }
  ],
  "pagination": {
    "count": 2,
    "limit": 25,
    "next_cursor": null
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 429 Rate limited · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Malformed query or request payload.

Usually happens when:
The request sends malformed query/body data.
What to do:
Validate request fields against the contract examples before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Missing plan access, capability, IP access, or scopes for this request.

Usually happens when:
The key is valid but the company’s plan does not include this (reads need a paid plan, writes need Premium or above), API access is suspended, the key IP allowlist does not match, or the key lacks the required scope.
What to do:
Upgrade the plan for writes, adjust the key IP allowlist, or create a key with the required scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle request volume and avoid immediate fan-out retries from parallel workers.
Retry guidance:
Honor the `Retry-After` header and use exponential backoff for repeated limits.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this request.
What to do:
Check API availability, then retry with backoff.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while reading or serializing the resource.
What to do:
Log the contract version and response body, then retry once before escalating to Asteri support.
Error response
{
  "error": "We couldn't load customers. Try again."
}
POST
/api/public/v1/workflows/{workflowId}/enrollmentsEnroll one customer in a workflow. Send an Idempotency-Key header; a retry returns the first answer.

Required permissions

workflows:write

Success:201

Usage

cURL
curl -X POST \
  'http://localhost:3000/api/public/v1/workflows/8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3/enrollments' \
  -H "Authorization: Bearer ast_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{
  "customer_id": "2e8a1d4c-7f3b-4a6e-8c1d-5b9f0e2a7c31"
}'
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.workflowsEnrollments.create({ workflowId: '8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3' }, {
  "customer_id": "2e8a1d4c-7f3b-4a6e-8c1d-5b9f0e2a7c31"
});

Request body

Body for enrolling one customer. Send an Idempotency-Key header with a unique value too.

WorkflowEnrollmentRequest
{
  "customer_id": "2e8a1d4c-7f3b-4a6e-8c1d-5b9f0e2a7c31"
}
View request fields
WorkflowEnrollmentRequest fields
FieldTypeRequiredDescription
customer_iduuidYesThe customer to put in the workflow. Must belong to this company.

Response body

WorkflowEnrollResponse
{
  "data": {
    "outcome": "enrolled",
    "message": null,
    "enrollment": {
      "id": "4b7c3d8e-6f2a-4c9b-8e7d-1a3f5b9c7e23",
      "workflow_id": "1f0c3c2e-6b1a-4c7e-9d2f-3a5b7c9e1f20",
      "customer_id": "2e8a1d4c-7f3b-4a6e-8c1d-5b9f0e2a7c31",
      "status": "active",
      "enrolled_at": "2026-10-23T12:00:00.000Z",
      "completed_at": null,
      "exited_at": null,
      "exit_reason": null
    }
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 404 Not found · 409 Conflict · 429 Rate limited · 413 Request too large · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Invalid request payload.

Usually happens when:
The request body is missing required fields or includes invalid field values.
What to do:
Validate the payload against the generated request schema and fix any `issues` entries before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Creating and changing records needs the Premium plan or above.

Usually happens when:
The company plan includes reading through the API but not writing, or the key lacks the write scope.
What to do:
Upgrade to Premium in Subscription settings, or create a key with the needed write scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

404 Not found

The workflow or customer is not in this company.

Usually happens when:
The workflow id or customer id is not one of this company’s records.
What to do:
Look the workflow and customer up in this company, then try again.
Error response
{
  "error": "We couldn't find that customer. Check its ID and try again."
}

409 Conflict

The workflow is paused, draft or archived, or the customer cannot join it right now.

Usually happens when:
The workflow is not on, its trigger is not connected, the customer is merged or has used up the workflow’s limit, or the Idempotency-Key was used for another customer.
What to do:
Read the message: turn the workflow on, or use a new Idempotency-Key for a different request.
Error response
{
  "error": "A customer already uses this email. Use a different email or open the existing customer.",
  "field": "email"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle mutation throughput and avoid blind retries from queues or worker pools.
Retry guidance:
Honor the `Retry-After` header and replay writes only after the relevant limit window resets.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

413 Request too large

The JSON request body exceeds this route’s byte limit.

Usually happens when:
The request body is larger than this write route accepts.
What to do:
Reduce the request body and try again.
Error response
{
  "error": "The request is too large. Send a smaller JSON body and try again."
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this write.
What to do:
Check API availability, then retry with the same idempotency key when applicable.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while validating or persisting the mutation.
What to do:
Capture the response payload and contract version, then retry once before escalating.
Error response
{
  "error": "We couldn't load customers. Try again."
}
DELETE
/api/public/v1/workflows/{workflowId}/enrollments/{customerId}Remove a customer from a workflow. The open place ends as exited; repeating the request changes nothing.

Required permissions

workflows:write

Success:200

Usage

cURL
curl -X DELETE \
  'http://localhost:3000/api/public/v1/workflows/8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3/enrollments/8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.workflowsEnrollments.remove({ workflowId: '8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3', customerId: '8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3' });

Response body

WorkflowExitResponse
{
  "data": {
    "outcome": "exited",
    "enrollment": {
      "id": "4b7c3d8e-6f2a-4c9b-8e7d-1a3f5b9c7e23",
      "workflow_id": "1f0c3c2e-6b1a-4c7e-9d2f-3a5b7c9e1f20",
      "customer_id": "2e8a1d4c-7f3b-4a6e-8c1d-5b9f0e2a7c31",
      "status": "exited",
      "enrolled_at": "2026-10-23T12:00:00.000Z",
      "completed_at": null,
      "exited_at": "2026-10-23T12:05:00.000Z",
      "exit_reason": "manual_exit"
    }
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 404 Not found · 409 Conflict · 429 Rate limited · 413 Request too large · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Invalid request payload.

Usually happens when:
The request body is missing required fields or includes invalid field values.
What to do:
Validate the payload against the generated request schema and fix any `issues` entries before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Creating and changing records needs the Premium plan or above.

Usually happens when:
The company plan includes reading through the API but not writing, or the key lacks the write scope.
What to do:
Upgrade to Premium in Subscription settings, or create a key with the needed write scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

404 Not found

The workflow is not in this company, or the customer was never in it.

Usually happens when:
The customer has no place in this workflow.
What to do:
Check the customer and workflow ids, then try again.
Error response
{
  "error": "We couldn't find that customer. Check its ID and try again."
}

409 Conflict

The place is being processed right now, or it changed while it was being removed.

Usually happens when:
A workflow step is running for this customer.
What to do:
Wait a minute, then send the request again.
Error response
{
  "error": "A customer already uses this email. Use a different email or open the existing customer.",
  "field": "email"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle mutation throughput and avoid blind retries from queues or worker pools.
Retry guidance:
Honor the `Retry-After` header and replay writes only after the relevant limit window resets.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

413 Request too large

The JSON request body exceeds this route’s byte limit.

Usually happens when:
The request body is larger than this write route accepts.
What to do:
Reduce the request body and try again.
Error response
{
  "error": "The request is too large. Send a smaller JSON body and try again."
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this write.
What to do:
Check API availability, then retry with the same idempotency key when applicable.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while validating or persisting the mutation.
What to do:
Capture the response payload and contract version, then retry once before escalating.
Error response
{
  "error": "We couldn't load customers. Try again."
}
View workflows schemas and field examples

Workflow

A company workflow that texts or emails customers. Its steps are not included.

Workflow fields
FieldTypeRequiredDescription
iduuidYesWorkflow id.
namestringYesThe name the company gave the workflow.
descriptionstring | nullYesWhat the workflow is for, or null when none was written.
statusactive | paused | draft | archivedYesactive: on and published. paused: published but turned off. draft: never published. archived: put away and left out of the list.
trigger_typestringYesWhat starts the workflow for a customer, such as manual or a job event.
enrollablebooleanYesTrue when the workflow is active and its trigger is connected, so customers can be enrolled through the API.
Example Workflow
{
  "id": "1f0c3c2e-6b1a-4c7e-9d2f-3a5b7c9e1f20",
  "name": "New customer welcome",
  "description": null,
  "status": "active",
  "trigger_type": "manual",
  "enrollable": true
}

WorkflowEnrollment

One customer’s place in a workflow.

WorkflowEnrollment fields
FieldTypeRequiredDescription
iduuidYesPlace id.
workflow_iduuidYesThe workflow the place is in.
customer_iduuidYesThe customer in the workflow.
statusactive | paused | completed | exited | failed | nullYesWhere the customer is in the workflow.
enrolled_atdate-time | nullYesWhen the customer joined the workflow.
completed_atdate-time | nullYesWhen the customer finished the workflow, or null.
exited_atdate-time | nullYesWhen the customer left the workflow early, or null.
exit_reasonstring | nullYesWhy the customer left, such as manual_exit, or null.
Example WorkflowEnrollment
{
  "id": "4b7c3d8e-6f2a-4c9b-8e7d-1a3f5b9c7e23",
  "workflow_id": "1f0c3c2e-6b1a-4c7e-9d2f-3a5b7c9e1f20",
  "customer_id": "2e8a1d4c-7f3b-4a6e-8c1d-5b9f0e2a7c31",
  "status": "active",
  "enrolled_at": "2026-10-23T12:00:00.000Z",
  "completed_at": null,
  "exited_at": null,
  "exit_reason": null
}

WorkflowEnrollmentRequest

Body for enrolling one customer. Send an Idempotency-Key header with a unique value too.

WorkflowEnrollmentRequest fields
FieldTypeRequiredDescription
customer_iduuidYesThe customer to put in the workflow. Must belong to this company.
Example WorkflowEnrollmentRequest
{
  "customer_id": "2e8a1d4c-7f3b-4a6e-8c1d-5b9f0e2a7c31"
}

Team members

Read the people on the company team.

/api/public/v1/team-members
GET
/api/public/v1/team-membersList active team members. Pass include_inactive=true to include people who have been deactivated.

Required permissions

team_members:read

Success:200

Usage

cURL
curl -X GET \
  'http://localhost:3000/api/public/v1/team-members' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.teamMembers.list();

Response body

TeamMemberListResponse
{
  "data": [
    {
      "id": "3a9b2c7d-5e1f-4b8a-9d6c-0f2e4a8b6d12",
      "first_name": "Sam",
      "last_name": "Tech",
      "display_name": "Sam Tech",
      "role": "technician",
      "role_label": "Technician",
      "job_title": "Lead installer",
      "is_active": true,
      "can_be_scheduled": true
    }
  ],
  "pagination": {
    "count": 2,
    "limit": 25,
    "next_cursor": null
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 429 Rate limited · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Malformed query or request payload.

Usually happens when:
The request sends malformed query/body data.
What to do:
Validate request fields against the contract examples before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Missing plan access, capability, IP access, or scopes for this request.

Usually happens when:
The key is valid but the company’s plan does not include this (reads need a paid plan, writes need Premium or above), API access is suspended, the key IP allowlist does not match, or the key lacks the required scope.
What to do:
Upgrade the plan for writes, adjust the key IP allowlist, or create a key with the required scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle request volume and avoid immediate fan-out retries from parallel workers.
Retry guidance:
Honor the `Retry-After` header and use exponential backoff for repeated limits.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this request.
What to do:
Check API availability, then retry with backoff.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while reading or serializing the resource.
What to do:
Log the contract version and response body, then retry once before escalating to Asteri support.
Error response
{
  "error": "We couldn't load customers. Try again."
}
GET
/api/public/v1/team-members/{teamMemberId}Read one team member in this company.

Required permissions

team_members:read

Success:200

Usage

cURL
curl -X GET \
  'http://localhost:3000/api/public/v1/team-members/8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3' \
  -H "Authorization: Bearer ast_live_your_key"
TypeScript SDK
import { AsteriPublicApiClient } from '@asteri/public-api-sdk';

const client = new AsteriPublicApiClient({
  baseUrl: 'http://localhost:3000',
  apiKey: process.env.ASTERI_API_KEY!,
});

const response = await client.teamMembers.get({ teamMemberId: '8af2ca8e-8f31-4e6e-a46c-0703b61f7ca3' });

Response body

TeamMemberResponse
{
  "data": {
    "id": "3a9b2c7d-5e1f-4b8a-9d6c-0f2e4a8b6d12",
    "first_name": "Sam",
    "last_name": "Tech",
    "display_name": "Sam Tech",
    "role": "technician",
    "role_label": "Technician",
    "job_title": "Lead installer",
    "is_active": true,
    "can_be_scheduled": true
  }
}
Common errors: 400 Bad request · 401 Unauthorized · 403 Forbidden · 404 Not found · 429 Rate limited · 503 Temporarily unavailable · 500 Internal error

400 Bad request

Malformed query or request payload.

Usually happens when:
The request sends malformed query/body data.
What to do:
Validate request fields against the contract examples before retrying.
Error response
{
  "error": "Invalid request payload",
  "issues": [
    {
      "path": "email",
      "message": "Invalid email address"
    }
  ]
}

401 Unauthorized

Missing, invalid, or expired API credentials.

Usually happens when:
The API key is invalid, revoked, archived, expired, or missing a required expiry.
What to do:
Verify the key value, status, expiration, and Authorization: Bearer ast_live_... header.
Error response
{
  "error": "invalid_api_key",
  "message": "This API key is not valid. Check the key and try again.",
  "request_id": "req_example_123"
}

403 Forbidden

Missing plan access, capability, IP access, or scopes for this request.

Usually happens when:
The key is valid but the company’s plan does not include this (reads need a paid plan, writes need Premium or above), API access is suspended, the key IP allowlist does not match, or the key lacks the required scope.
What to do:
Upgrade the plan for writes, adjust the key IP allowlist, or create a key with the required scope.
Error response
{
  "error": "developer_api_missing_scope",
  "message": "This API key does not have access to this request. Check its scopes and try again.",
  "request_id": "req_example_123"
}

404 Not found

No team member with this id in this company.

Usually happens when:
The id is not a team member of this company, or the account was deleted.
What to do:
Read the team list to find the right id.
Error response
{
  "error": "We couldn't find that customer. Check its ID and try again."
}

429 Rate limited

Source IP, API key, or organization request limit exceeded.

Usually happens when:
The source IP, current key, or organization has exceeded a request limit.
What to do:
Throttle request volume and avoid immediate fan-out retries from parallel workers.
Retry guidance:
Honor the `Retry-After` header and use exponential backoff for repeated limits.
Error response
{
  "error": "developer_api_rate_limited",
  "message": "Too many API requests. Please try again shortly.",
  "request_id": "req_example_123"
}

503 Temporarily unavailable

Developer API access or a required admission service is unavailable.

Usually happens when:
The API is disabled or its rate-limit service cannot safely admit this request.
What to do:
Check API availability, then retry with backoff.
Error response
{
  "error": "developer_api_rate_limit_unavailable",
  "message": "We could not check the request limit. Please try again shortly.",
  "request_id": "req_example_123"
}

500 Internal error

Unexpected API failure.

Usually happens when:
The API hit an unexpected internal failure while reading or serializing the resource.
What to do:
Log the contract version and response body, then retry once before escalating to Asteri support.
Error response
{
  "error": "We couldn't load customers. Try again."
}
View team members schemas and field examples

TeamMember

A person on the company team. Pay, contact details and private employment records are never included.

TeamMember fields
FieldTypeRequiredDescription
iduuidYesTeam member id.
first_namestring | nullYesFirst name.
last_namestring | nullYesLast name.
display_namestringYesThe name people go by: the preferred name when one is set.
rolestringYesThe stored role key, such as owner, admin or technician.
role_labelstringYesThe role as people see it.
job_titlestring | nullYesJob title shown on job pages, or null.
is_activebooleanYesFalse once the person has been deactivated.
can_be_scheduledbooleanYesWhether the person can be put on the schedule.
Example TeamMember
{
  "id": "3a9b2c7d-5e1f-4b8a-9d6c-0f2e4a8b6d12",
  "first_name": "Sam",
  "last_name": "Tech",
  "display_name": "Sam Tech",
  "role": "technician",
  "role_label": "Technician",
  "job_title": "Lead installer",
  "is_active": true,
  "can_be_scheduled": true
}
Asteri developer documentation · View changelog