API Documentation
Find API guidance quickly
Search authentication, endpoints, and request parameters.
Tenant API overview
Send requests to https://your-workspace.crewvana.com/api on your own workspace domain, not the support or central domain. Replace resource IDs and placeholders with values from that workspace. Numeric resource IDs are local to a workspace.
This reference covers all 38 routes registered in the tenant integration API: readiness, 22 read routes, 12 service resource writes, Form test submission and two legacy POST routes. Mobile/browser session endpoints, model locks, public customer forms, webhooks, MCP, and support-agent APIs are separate interfaces.
The read API retrieves Contacts, customer Subscriptions and their definitions/occurrences, Jobs, Visits, Invoices, payment history, Forms, submissions, and Automation state. General CRUD is available for Subscriptions, Jobs and Visits; supported lifecycle actions preserve execution and billing history. It does not provide general Contact/Form/Automation creation, update, deletion, publication, enrollment creation, or message sending. Opportunities belong in pipelines; Contacts represent customer identity. The registered Contact pipeline route is obsolete and is not an Opportunity movement API.
Authenticating
The 22 read endpoints, service resource writes and Form test submission accept Authorization: Bearer YOUR_API_KEY or HTTP Basic authentication with YOUR_API_KEY as the username and a blank password. Create and manage keys in Settings > API. These routes do not accept an api_key query/body parameter.
If the key requires an IP allowlist, the caller must match an allowed IP address or configured IPv4 range. Missing/invalid keys return 401; a disallowed IP returns 403. Keys grant workspace-wide reads and Form test submission. Service resource writes also require an active permission-checked actor and retry identity; see Service resource writes and input contracts.
Use HTTPS, store keys on the server, and send Accept: application/json on every request. Never put keys in browser code or share them. The readiness route needs no key. The two legacy routes do not use the read API authentication middleware; see their endpoint limitations.
API limitations and pagination
The authenticated reads, service resource writes and Form test submission share Laravel’s throttle:60,1 middleware: 60 requests per minute. Its default rate-limit identity is the caller IP; rotating keys does not provide a separate bucket. A 429 response includes Retry-After and rate-limit headers. The readiness and legacy routes are outside this explicit throttle.
Paginated results provide count, offset, next_offset, and has_more. Count is the returned page size, not the total. Continue with next_offset until it is null; keep the original filters. Do not exceed the endpoint’s documented maximum offset. There is no cursor or total-count pagination.
Query booleans should be sent as 0 or 1. Dates are inclusive comparisons against stored timestamps, not calendar-day ranges. Use explicit timestamp bounds; a to date by itself can exclude most of that day. Ordering and snapshots can change between requests; offset pages are not transactionally frozen.
Detail responses have bounded related data. A has_more or has_earlier flag means truncation, not a continuation token. No separate API cursor exists for Contact detail history. Text/payload fields may be truncated or sanitized; known credential-like payload keys are replaced with [redacted]. This is not a guarantee that free-text fields contain no sensitive information.
Errors and troubleshooting
Always send Accept: application/json so validation failures return JSON instead of a browser redirect. Check HTTP status before interpreting a response; not every error uses the same envelope.
A valid numeric resource ID belonging to a different workspace is not readable through your workspace’s endpoint. Unknown numeric IDs return 404. Unknown paths and some unsupported methods fall through to the tenant API’s 421 response. Non-numeric detail IDs also miss the numeric routes and can receive 421.
Authenticated endpoint validation uses HTTP 422 with message and errors (field-name arrays). IP/authentication/not-found errors use success: false plus message. A legacy plan failure uses HTTP 422 with upgrade_required, required_plan, feature, feature_label, upgrade_url, message, and errors.general_error. Plan names come from current configuration; no fixed paid tier is guaranteed here.
If a request fails: verify the workspace host, Authorization header, IP allowlist, HTTP method, numeric IDs, required query, parameter bounds, and workspace plan. After 429, wait for Retry-After; retry safe read requests with backoff. Do not blindly retry a legacy trigger because dispatch can repeat.
Status codes
| Property | Format | Description |
|---|---|---|
401 | Authentication | API key required or invalid (read and Form test routes). |
403 | IP restriction | Caller IP is not allowed for the key (read and Form test routes). |
404 | Missing resource | Contact, Subscription, Invoice, Form, or Automation not found. |
421 | Unknown route or method | API path/method not registered; fallback response has success: false and message. |
422 | Validation or plan gate | Correct errors; inspect upgrade_required for legacy plan failures. |
429 | Throttled | Wait for Retry-After before retrying. |
500 | Server error | Unexpected failure; the legacy pipeline placeholder can fail while wrapping its response. |
Illustrative response
{
"message": "The query field is required.",
"errors": {
"query": [
"The query field is required."
]
}
}
Check API readiness
ANY /api
Authentication: None
No authentication is required. Confirms routing/tenant initialization, not API-key validity, permissions, feature availability, or successful downstream processing. Use GET for the example.
No request parameters are required.
Response fields
| Property | Format | Description |
|---|---|---|
success | Boolean | True for successful read responses. |
message | String | API ready. |
Illustrative response
{
"success": true,
"message": "API ready."
}
cURL
curl 'https://your-workspace.crewvana.com/api' \
--header 'Accept: application/json'
PHP
<?php
$request = curl_init('https://your-workspace.crewvana.com/api');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => ['Accept: application/json'],
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($request);
curl_close($request);
echo $response;
NodeJs
// Node.js 18 or later, in an ES module (.mjs). Keep API keys on the server.
const response = await fetch('https://your-workspace.crewvana.com/api', {
method: 'GET',
headers: {
Accept: 'application/json',
},
});
console.log(await response.json());
Python
# Install requests with: python -m pip install requests
import requests
response = requests.request(
'GET',
'https://your-workspace.crewvana.com/api',
headers={'Accept': 'application/json'},
timeout=30,
)
print(response.json())
Ruby
require 'net/http'
require 'uri'
url = URI('https://your-workspace.crewvana.com/api')
request = Net::HTTP::Get.new(url)
request['Accept'] = 'application/json'
response = Net::HTTP.start(url.hostname, url.port, use_ssl: true) do |http|
http.request(request)
end
puts response.body
Search Contacts
GET /api/contacts
Authentication: API key (Bearer or Basic)
A required search query prevents unbounded listing. Archived Contacts are included by default. Search also checks exact numeric IDs, company names, email addresses, and phone values.
Input
| Parameter | Format | Required | Description |
|---|---|---|---|
query | String | Required | 2–120 characters before trimming. Search by name or numeric ID, company, email, or phone. |
limit | Integer | Optional | 1–25; default 10. |
offset | Integer | Optional | 0–1000; default 0. |
include_archived | Boolean | Optional | Default true. Send 0 to exclude archived/inactive records; use 0 or 1 in query strings. |
Response fields
| Property | Format | Description |
|---|---|---|
success | Boolean | True for successful read responses. |
contacts | Array | id, full_name, first_name, last_name, company_name, status, archived, source, created_at, updated_at, conversation_count, service_job_count, subscription_count. archived is Boolean; counts are integers. Sorted by full_name, then id. |
count | Integer | Number of rows on this page, not the total match count. |
offset | Integer | Current offset. |
next_offset | Integer or null | Pass as offset for the next page; null when no further page exists. |
has_more | Boolean | Whether more matching rows exist. |
Illustrative response
{
"success": true,
"contacts": [],
"count": 0,
"offset": 0,
"next_offset": null,
"has_more": false
}
cURL
curl 'https://your-workspace.crewvana.com/api/contacts?query=Example&limit=10' \
--header 'Accept: application/json' \
--user 'YOUR_API_KEY:'
PHP
<?php
$request = curl_init('https://your-workspace.crewvana.com/api/contacts?query=Example&limit=10');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => ['Accept: application/json'],
CURLOPT_USERPWD => 'YOUR_API_KEY:',
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($request);
curl_close($request);
echo $response;
NodeJs
// Node.js 18 or later, in an ES module (.mjs). Keep API keys on the server.
const response = await fetch('https://your-workspace.crewvana.com/api/contacts?query=Example&limit=10', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Basic ' + Buffer.from('YOUR_API_KEY:').toString('base64'),
},
});
console.log(await response.json());
Python
# Install requests with: python -m pip install requests
import requests
response = requests.request(
'GET',
'https://your-workspace.crewvana.com/api/contacts?query=Example&limit=10',
headers={'Accept': 'application/json'},
auth=('YOUR_API_KEY', ''),
timeout=30,
)
print(response.json())
Ruby
require 'net/http'
require 'uri'
url = URI('https://your-workspace.crewvana.com/api/contacts?query=Example&limit=10')
request = Net::HTTP::Get.new(url)
request['Accept'] = 'application/json'
request.basic_auth('YOUR_API_KEY', '')
response = Net::HTTP.start(url.hostname, url.port, use_ssl: true) do |http|
http.request(request)
end
puts response.body
Read a Contact’s Invoices
GET /api/contacts/{contact}/invoices
Authentication: API key (Bearer or Basic)
Returns persisted invoice status, job association/status, totals, completed-payment sum, balance, payment count, and link existence/expiry/used timestamps. Missing Contact returns 404; an existing Contact with no matches returns an empty list. Ordered by created_at descending, then ID descending.
paid_amount sums completed invoice_amount entries, including negative refunds, and excludes tips, pending and failed payments. document_status is Draft, Issued or Cancelled; payment_status is Unpaid, Partial or Paid; delivery_status is Not sent, Sent, Delivered or Failed. status remains a compatibility field. balance_due is zero for Cancelled invoices, otherwise max(0, total minus paid_amount). Issuance and delivery timestamps require actual evidence; copying a link does not count as sending. Reads never synchronize state or issue links. Tokens and provider secrets are excluded.
Input
| Parameter | Format | Required | Description |
|---|---|---|---|
contact | Integer (path) | Required | Contact ID in this workspace. |
service_job_id | Integer | Optional | Positive Job ID; exact match within this Contact. |
invoice_number | String | Optional | Exact invoice number, 1–120 characters. Filters combine with AND. |
limit | Integer | Optional | 1–25; default 10. |
offset | Integer | Optional | 0–1000; default 0. |
Response fields
| Property | Format | Description |
|---|---|---|
tenant_id, generated_at_utc, contact_id | Identity and timestamp | Active workspace, UTC generation time and requested Contact. Stored timestamps use the Contact read API representation. |
invoices | Array | id, contact_id, service_job_id, opportunity_id, invoice_number, status, document_status, payment_status, delivery_status, issued_at, sent_at, delivered_at, failed_at, cancelled_at, cancelled_by_user_id, cancellation_reason, overdue, can_collect_payment, sub_total, tax, total, due_date, created_at, updated_at, job_status, job_paid_status, paid_amount, balance_due, payment_count, has_payment_link, payment_link_expires_at, payment_link_used_at. |
count, offset, next_offset, has_more | Pagination | Returned count, current offset, next offset or null, and truncation flag. |
Illustrative response
{
"success": true,
"tenant_id": "example",
"contact_id": 123,
"invoices": [],
"count": 0,
"offset": 0,
"next_offset": null,
"has_more": false
}
cURL
curl 'https://your-workspace.crewvana.com/api/contacts/{contact}/invoices?limit=10' \
--header 'Accept: application/json' \
--user 'YOUR_API_KEY:'
PHP
<?php
$request = curl_init('https://your-workspace.crewvana.com/api/contacts/{contact}/invoices?limit=10');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => ['Accept: application/json'],
CURLOPT_USERPWD => 'YOUR_API_KEY:',
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($request);
curl_close($request);
echo $response;
NodeJs
// Node.js 18 or later, in an ES module (.mjs). Keep API keys on the server.
const response = await fetch('https://your-workspace.crewvana.com/api/contacts/{contact}/invoices?limit=10', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Basic ' + Buffer.from('YOUR_API_KEY:').toString('base64'),
},
});
console.log(await response.json());
Python
# Install requests with: python -m pip install requests
import requests
response = requests.request(
'GET',
'https://your-workspace.crewvana.com/api/contacts/{contact}/invoices?limit=10',
headers={'Accept': 'application/json'},
auth=('YOUR_API_KEY', ''),
timeout=30,
)
print(response.json())
Ruby
require 'net/http'
require 'uri'
url = URI('https://your-workspace.crewvana.com/api/contacts/{contact}/invoices?limit=10')
request = Net::HTTP::Get.new(url)
request['Accept'] = 'application/json'
request.basic_auth('YOUR_API_KEY', '')
response = Net::HTTP.start(url.hostname, url.port, use_ssl: true) do |http|
http.request(request)
end
puts response.body
Read an Invoice and its Payments
GET /api/invoices/{invoice}
Authentication: API key (Bearer or Basic)
Returns the same invoice summary as the Contact invoice list plus payment history ordered by paid_at descending, then ID descending. Missing invoice returns 404. Invoice aggregates use every completed transaction regardless of history pagination. Return payments.next_offset as payment_offset to continue; retain payment_limit.
Payment rows include every status as stored. Reads make no business-data changes and reveal no provider payloads, external references, idempotency keys or card details. Link metadata does not prove delivery; compare Contact conversation events for sending and delivery evidence. Separate reads are not a transactionally frozen snapshot.
Input
| Parameter | Format | Required | Description |
|---|---|---|---|
invoice | Integer (path) | Required | Invoice ID in this workspace. |
payment_limit | Integer | Optional | 1–100; default 50. |
payment_offset | Integer | Optional | 0–5000; default 0. |
Response fields
| Property | Format | Description |
|---|---|---|
tenant_id, generated_at_utc | Identity and timestamp | Active workspace and generation time in UTC. |
invoice | Object | Same summary fields as GET /api/contacts/{contact}/invoices. |
payments.items | Array | id, invoice_id, service_job_id, contact_id, opportunity_id, method, source, provider, status, amount, invoice_amount, tip_amount, paid_at, created_at, updated_at. |
payments.count, payments.offset, payments.next_offset, payments.has_more | Pagination | Returned count, current payment offset, next offset or null, and truncation flag. |
Illustrative response
{
"success": true,
"tenant_id": "example",
"invoice": {
"id": 456,
"status": "Draft",
"total": "75.00",
"paid_amount": 0,
"balance_due": 75,
"payment_count": 0
},
"payments": {
"items": [],
"count": 0,
"offset": 0,
"next_offset": null,
"has_more": false
}
}
cURL
curl 'https://your-workspace.crewvana.com/api/invoices/{invoice}?payment_limit=50' \
--header 'Accept: application/json' \
--user 'YOUR_API_KEY:'
PHP
<?php
$request = curl_init('https://your-workspace.crewvana.com/api/invoices/{invoice}?payment_limit=50');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => ['Accept: application/json'],
CURLOPT_USERPWD => 'YOUR_API_KEY:',
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($request);
curl_close($request);
echo $response;
NodeJs
// Node.js 18 or later, in an ES module (.mjs). Keep API keys on the server.
const response = await fetch('https://your-workspace.crewvana.com/api/invoices/{invoice}?payment_limit=50', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Basic ' + Buffer.from('YOUR_API_KEY:').toString('base64'),
},
});
console.log(await response.json());
Python
# Install requests with: python -m pip install requests
import requests
response = requests.request(
'GET',
'https://your-workspace.crewvana.com/api/invoices/{invoice}?payment_limit=50',
headers={'Accept': 'application/json'},
auth=('YOUR_API_KEY', ''),
timeout=30,
)
print(response.json())
Ruby
require 'net/http'
require 'uri'
url = URI('https://your-workspace.crewvana.com/api/invoices/{invoice}?payment_limit=50')
request = Net::HTTP::Get.new(url)
request['Accept'] = 'application/json'
request.basic_auth('YOUR_API_KEY', '')
response = Net::HTTP.start(url.hostname, url.port, use_ssl: true) do |http|
http.request(request)
end
puts response.body
Read a Contact’s Subscriptions
GET /api/contacts/{contact}/subscriptions
Authentication: API key (Bearer or Basic)
Lists customer service agreements, newest ID first, independently of platform billing. Missing Contact returns 404. An existing Contact without matches returns an empty page. Reads never activate, generate, schedule or repair agreements.
Input
| Parameter | Format | Required | Description |
|---|---|---|---|
contact | Integer (path) | Required | Contact ID in this workspace. |
status | String | Optional | Exact draft, quoted, scheduled, active, paused, ended, expired, cancelled or archived lifecycle. |
limit | Integer | Optional | 1–100; default 25. |
offset | Integer | Optional | 0–10000; default 0. |
Response fields
| Property | Format | Description |
|---|---|---|
tenant_id, generated_at_utc | Identity and timestamp | Active workspace and response creation time in UTC. |
subscriptions.items | Array | Agreement IDs, Contact/Opportunity/plan links, lifecycle, timezone, effective range, lifecycle timestamps, generation horizon, occurrence limit, current version, billing terms, tax flag, per-service total, legacy start/end and creation/update timestamps. plan contains ID, name, billing_interval and billing_frequency. |
subscriptions.count, offset, has_more, next_offset | Pagination | Page count, current offset, continuation flag and next offset or null. |
Illustrative response
{
"success": true,
"tenant_id": "example",
"contact_id": 123,
"subscriptions": {
"items": [],
"count": 0,
"offset": 0,
"has_more": false,
"next_offset": null
}
}
cURL
curl 'https://your-workspace.crewvana.com/api/contacts/123/subscriptions?limit=25' \
--header 'Accept: application/json' \
--user 'YOUR_API_KEY:'
PHP
<?php
$request = curl_init('https://your-workspace.crewvana.com/api/contacts/123/subscriptions?limit=25');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => ['Accept: application/json'],
CURLOPT_USERPWD => 'YOUR_API_KEY:',
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($request);
curl_close($request);
echo $response;
NodeJs
// Node.js 18 or later, in an ES module (.mjs). Keep API keys on the server.
const response = await fetch('https://your-workspace.crewvana.com/api/contacts/123/subscriptions?limit=25', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Basic ' + Buffer.from('YOUR_API_KEY:').toString('base64'),
},
});
console.log(await response.json());
Python
# Install requests with: python -m pip install requests
import requests
response = requests.request(
'GET',
'https://your-workspace.crewvana.com/api/contacts/123/subscriptions?limit=25',
headers={'Accept': 'application/json'},
auth=('YOUR_API_KEY', ''),
timeout=30,
)
print(response.json())
Ruby
require 'net/http'
require 'uri'
url = URI('https://your-workspace.crewvana.com/api/contacts/123/subscriptions?limit=25')
request = Net::HTTP::Get.new(url)
request['Accept'] = 'application/json'
request.basic_auth('YOUR_API_KEY', '')
response = Net::HTTP.start(url.hostname, url.port, use_ssl: true) do |http|
http.request(request)
end
puts response.body
Read a Subscription Definition
GET /api/subscriptions/{subscription}
Authentication: API key (Bearer or Basic)
Returns the customer agreement summary, its latest saved definition, recurrence, commercial Job lines, ordered Visit preferences and generation cursor. Billing Frequency and saved service recurrence are separate fields; a plan name alone does not establish recurrence. Missing Subscription returns 404.
current_definition is null before a definition exists. Definition contains legacy_backfill without exposing arbitrary snapshots. Job templates are capped at 25; each has up to 100 commercial lines and 25 Visit templates. Legacy items/packages/discounts are each capped at 100. Each nested page reports has_more; nested truncation is not a follow-up cursor for this endpoint. Provider payloads, arbitrary metadata, constraints and credentials are omitted. No business records change.
Input
| Parameter | Format | Required | Description |
|---|---|---|---|
subscription | Integer (path) | Required | Customer Subscription ID in this workspace. |
Response fields
| Property | Format | Description |
|---|---|---|
subscription | Object | Same summary and plan fields as the Contact Subscription list. |
current_definition | Object or null | ID/version/state, timezone, effective range, initial Visit date, horizon/limit, short-month and DST policies, week start, legacy_backfill, actor/frozen/creation/update times. recurrence contains frequency, interval, weekly_days, monthly mode/day/week/weekday, annual month/day and local_time. |
current_definition.job_templates | Page object | Ordered Job IDs/stable keys/sequences/names, cycle offsets, tax flags, lines and visits. Lines expose type, Price Book IDs, name, quantity, amount and duration; Visits expose IDs, stable keys, sequence, name, offsets, duration, preference mode and preferred start/end. |
generation_state | Object or null | Last cadence/date, generated-through date, last run/success and retry count. |
legacy_commercial_lines | Object | items, packages and discounts page objects; each row includes ID, Price Book ID, quantity and amount. |
Illustrative response (selected fields)
{
"success": true,
"tenant_id": "example",
"subscription": {
"id": 456,
"status": "draft"
},
"current_definition": null,
"generation_state": null
}
Read Subscription Occurrences
GET /api/subscriptions/{subscription}/occurrences
Authentication: API key (Bearer or Basic)
Lists saved Job occurrences ordered by local_slot_date, cadence sequence and ID. from_date/to_date compare inclusive local calendar dates, not UTC timestamps. Generated Job data is returned only when its Subscription matches the requested agreement. Missing/broken Job links are null. Reads do not generate, retry dispatch or alter schedules.
Each occurrence includes up to 25 saved Visit occurrences and 25 scheduling conflicts, with explicit nested truncation flags. Visit occurrence resolved times show generation outcomes; use Contact detail Jobs/Visits for current persisted execution schedules. No arbitrary snapshots, hashes, conflict details or failure payloads are returned.
Input
| Parameter | Format | Required | Description |
|---|---|---|---|
subscription | Integer (path) | Required | Customer Subscription ID in this workspace. |
limit | Integer | Optional | 1–100; default 25. |
offset | Integer | Optional | 0–10000; default 0. |
from_date | Date | Optional | Inclusive local slot date, YYYY-MM-DD. |
to_date | Date | Optional | Inclusive local slot date, YYYY-MM-DD; at or after from_date when supplied. |
Response fields
| Property | Format | Description |
|---|---|---|
occurrences.items | Array | Occurrence ID, version/template IDs, cadence_sequence, local_slot_date, linked service_job_id, state and timestamps. job contains Contact ID, status/payment state, subtotal/total, completion/cancellation and creation/update times. |
occurrences.items[].visits | Page object | Visit occurrence ID, Visit ID, sequence, requested local date/start/end, resolved UTC start/end and state. |
occurrences.items[].conflicts | Page object | Conflict ID, Visit occurrence ID, code, severity, state and resolution timestamp. |
occurrences.count, offset, has_more, next_offset | Pagination | Use next_offset with the same filters until null. |
Illustrative response
{
"success": true,
"tenant_id": "example",
"subscription_id": 456,
"occurrences": {
"items": [],
"count": 0,
"offset": 0,
"has_more": false,
"next_offset": null
}
}
cURL
curl 'https://your-workspace.crewvana.com/api/subscriptions/456/occurrences?limit=25' \
--header 'Accept: application/json' \
--user 'YOUR_API_KEY:'
PHP
<?php
$request = curl_init('https://your-workspace.crewvana.com/api/subscriptions/456/occurrences?limit=25');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => ['Accept: application/json'],
CURLOPT_USERPWD => 'YOUR_API_KEY:',
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($request);
curl_close($request);
echo $response;
NodeJs
// Node.js 18 or later, in an ES module (.mjs). Keep API keys on the server.
const response = await fetch('https://your-workspace.crewvana.com/api/subscriptions/456/occurrences?limit=25', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Basic ' + Buffer.from('YOUR_API_KEY:').toString('base64'),
},
});
console.log(await response.json());
Python
# Install requests with: python -m pip install requests
import requests
response = requests.request(
'GET',
'https://your-workspace.crewvana.com/api/subscriptions/456/occurrences?limit=25',
headers={'Accept': 'application/json'},
auth=('YOUR_API_KEY', ''),
timeout=30,
)
print(response.json())
Ruby
require 'net/http'
require 'uri'
url = URI('https://your-workspace.crewvana.com/api/subscriptions/456/occurrences?limit=25')
request = Net::HTTP::Get.new(url)
request['Accept'] = 'application/json'
request.basic_auth('YOUR_API_KEY', '')
response = Net::HTTP.start(url.hostname, url.port, use_ssl: true) do |http|
http.request(request)
end
puts response.body
Read a Contact
GET /api/contacts/{contact}
Authentication: API key (Bearer or Basic)
Returns an operational snapshot with identity, conversations, Jobs, subscriptions, failures, and audit history. A missing Contact returns 404. Related collections are bounded; has_more/has_earlier do not provide a follow-up cursor.
Input
| Parameter | Format | Required | Description |
|---|---|---|---|
contact | Integer (path) | Required | Numeric Contact ID in this workspace. |
event_limit | Integer | Optional | 1–500; default 200. Latest events across the returned conversations. |
audit_limit | Integer | Optional | 1–250; default 100. |
Response fields
| Property | Format | Description |
|---|---|---|
success | Boolean | True for successful read responses. |
tenant_id | String | Workspace identifier. |
generated_at_utc | String | UTC timestamp when the detail snapshot was generated. |
contact | Object | id, full_name, first_name, last_name, company_name, status, archived, source, created_at, updated_at, campaign_source, channel_source, timezone. archived is Boolean. |
identity | Object | phones, emails, addresses, shared_identity_contacts. Phone/email rows: id, contact_id, phone or email, type, is_primary (Boolean), sort_order, created_at, updated_at. Address rows: id, contact_id, is_primary, sort_order, address_line1, address_line2, city, state, postal_code, country, created_at, updated_at. Shared identities: identity_type, contact_id, full_name, archived, identity_value; may include the queried Contact. |
communication_preference | Object or null | id, contact_id, dnd_all, dnd_call, dnd_sms, dnd_email, dnd_incoming, marketing_email_subscribed, marketing_sms_subscribed, marketing_email_unsubscribed_at, marketing_sms_unsubscribed_at, preferred_method, created_at, updated_at. DND/subscription flags are Booleans. |
duplicate_history | Object | candidates (up to 100): id, contact_id_1, contact_id_2, match_criteria (decoded), match_value, status, resolved_by_user_id, resolved_at, created_at, updated_at. merges (up to 100): id, master_contact_id, secondary_contact_ids, contact_names, duplicate_ids (decoded), merged_by_user_id, restored_by_user_id, merged_at, restored_at, created_at, updated_at. |
conversations | Object | items (latest 50), count, has_more, endpoint_groups, events, event_count, has_earlier_events. Conversation rows: id, comm_channel_id, remote_endpoint_type, remote_endpoint, remote_endpoint_source, remote_endpoint_bound_at, is_open, is_starred, last_activity_at, created_at, updated_at, event_count, first_event_at, last_event_at, linked_contact_ids. Alias conversations are excluded when alias data exists. |
conversations.endpoint_groups | Array | comm_channel_id, remote_endpoint_type, remote_endpoint, conversation_ids, conversation_count for groups of returned conversation rows. |
conversations.events | Array | Latest event_limit events, returned in chronological order: id, comm_conversation_id, comm_channel_id, direction, event_type, status, transport_message_id, body (2,000 characters), payload (sanitized), user_id, occurred_at, created_at, updated_at. |
jobs | Object | items (latest 250), count, has_more. Job rows: id, contact_id, contact_subscription_id, opportunity_id, recurring_scheduled_date, contact_address_id, contact_phone_id, contact_email_id, status, paid_status, scheduled_by_user_id, sub_total, total, completed_at, cancelled_at, created_at, updated_at, visits, subscription_occurrence, message_logs. |
jobs.items[].visits | Array | id, service_job_id, sequence, is_default, scheduled_start, scheduled_end, status, cancelled_at, cancelled_by_user_id, created_at, updated_at, technicians. Technician rows: visit_id, user_id, name, checked_in_at, checked_out_at. |
jobs.items[].subscription_occurrence | Object or null | id, contact_subscription_id, contact_subscription_version_id, contact_subscription_job_template_id, cadence_sequence, local_slot_date, occurrence_key, service_job_id, state, failure_message, created_at, updated_at. |
jobs.items[].message_logs | Array | id, service_job_id, scenario_key, dedupe_key, sent_at, created_at, updated_at. |
subscriptions | Object | items (latest 100), count, has_more. Rows: id, contact_id, opportunity_id, price_book_subscription_id, status, timezone, effective_start_date, effective_end_date, activated_at, paused_at, cancelled_at, archived_at, generation_horizon_days, occurrence_limit, current_version_number, starts_at, ends_at, created_at, updated_at, job_occurrences. Occurrences share the Job occurrence fields above, with at most 1,000 occurrence rows across the returned subscriptions. |
failed_messages | Object | items (latest 100), count, has_more. Rows: id, contact_id, scenario_key, status, preferred_method, attempted_channel, failure_reason, last_error, retry_count, last_attempted_at, sent_at, created_at, updated_at. |
audit | Object | items (latest audit_limit), count, has_earlier. Rows: id, contact_id, user_id, action, field, old_value, new_value, created_at. Old/new values are truncated to 2,000 characters. |
Illustrative response (selected fields)
{
"success": true,
"tenant_id": "example-workspace",
"generated_at_utc": "2026-09-29T12:00:00+00:00",
"contact": {
"id": 123,
"full_name": "Example Customer",
"archived": false
},
"conversations": {
"items": [],
"count": 0,
"has_more": false,
"endpoint_groups": [],
"events": [],
"event_count": 0,
"has_earlier_events": false
}
}
cURL
curl 'https://your-workspace.crewvana.com/api/contacts/123?event_limit=20&audit_limit=20' \
--header 'Accept: application/json' \
--user 'YOUR_API_KEY:'
PHP
<?php
$request = curl_init('https://your-workspace.crewvana.com/api/contacts/123?event_limit=20&audit_limit=20');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => ['Accept: application/json'],
CURLOPT_USERPWD => 'YOUR_API_KEY:',
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($request);
curl_close($request);
echo $response;
NodeJs
// Node.js 18 or later, in an ES module (.mjs). Keep API keys on the server.
const response = await fetch('https://your-workspace.crewvana.com/api/contacts/123?event_limit=20&audit_limit=20', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Basic ' + Buffer.from('YOUR_API_KEY:').toString('base64'),
},
});
console.log(await response.json());
Python
# Install requests with: python -m pip install requests
import requests
response = requests.request(
'GET',
'https://your-workspace.crewvana.com/api/contacts/123?event_limit=20&audit_limit=20',
headers={'Accept': 'application/json'},
auth=('YOUR_API_KEY', ''),
timeout=30,
)
print(response.json())
Ruby
require 'net/http'
require 'uri'
url = URI('https://your-workspace.crewvana.com/api/contacts/123?event_limit=20&audit_limit=20')
request = Net::HTTP::Get.new(url)
request['Accept'] = 'application/json'
request.basic_auth('YOUR_API_KEY', '')
response = Net::HTTP.start(url.hostname, url.port, use_ssl: true) do |http|
http.request(request)
end
puts response.body
Search Forms
GET /api/forms
Authentication: API key (Bearer or Basic)
Search by Form name or exact numeric ID. Inactive Forms are included by default.
Input
| Parameter | Format | Required | Description |
|---|---|---|---|
query | String | Required | 1–120 characters before trimming. Search by name or numeric ID. |
limit | Integer | Optional | 1–25; default 10. |
offset | Integer | Optional | 0–1000; default 0. |
include_inactive | Boolean | Optional | Default true. Send 0 to exclude archived/inactive records; use 0 or 1 in query strings. |
Response fields
| Property | Format | Description |
|---|---|---|
success | Boolean | True for successful read responses. |
forms | Array | id, name, is_active, default_source, campaign_source, channel_source, pipeline_id, pipeline_status_id, created_at, updated_at, submission_count. Sorted by name, then id. |
count | Integer | Number of rows on this page, not the total match count. |
offset | Integer | Current offset. |
next_offset | Integer or null | Pass as offset for the next page; null when no further page exists. |
has_more | Boolean | Whether more matching rows exist. |
Illustrative response
{
"success": true,
"forms": [],
"count": 0,
"offset": 0,
"next_offset": null,
"has_more": false
}
cURL
curl 'https://your-workspace.crewvana.com/api/forms?query=Quote&limit=10' \
--header 'Accept: application/json' \
--user 'YOUR_API_KEY:'
PHP
<?php
$request = curl_init('https://your-workspace.crewvana.com/api/forms?query=Quote&limit=10');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => ['Accept: application/json'],
CURLOPT_USERPWD => 'YOUR_API_KEY:',
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($request);
curl_close($request);
echo $response;
NodeJs
// Node.js 18 or later, in an ES module (.mjs). Keep API keys on the server.
const response = await fetch('https://your-workspace.crewvana.com/api/forms?query=Quote&limit=10', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Basic ' + Buffer.from('YOUR_API_KEY:').toString('base64'),
},
});
console.log(await response.json());
Python
# Install requests with: python -m pip install requests
import requests
response = requests.request(
'GET',
'https://your-workspace.crewvana.com/api/forms?query=Quote&limit=10',
headers={'Accept': 'application/json'},
auth=('YOUR_API_KEY', ''),
timeout=30,
)
print(response.json())
Ruby
require 'net/http'
require 'uri'
url = URI('https://your-workspace.crewvana.com/api/forms?query=Quote&limit=10')
request = Net::HTTP::Get.new(url)
request['Accept'] = 'application/json'
request.basic_auth('YOUR_API_KEY', '')
response = Net::HTTP.start(url.hostname, url.port, use_ssl: true) do |http|
http.request(request)
end
puts response.body
Submit a Form test
POST /api/forms/test
Authentication: API key (Bearer or Basic)
Submit exactly one of form_id or form_name. Form IDs are workspace-local; form_name matches the full configured name using the database name comparison, with no partial search. Unknown Forms return 404. JSON and application/x-www-form-urlencoded bodies are supported. No browser session or CSRF token is required.
The server generates fresh dummy Contact details and valid configured fields, including supported custom fields and choices. Caller-supplied Contact details are ignored. The same Test action as Desktop runs normal persistence, attribution, tags, address/custom values, Opportunity routing, activity, Conversation events, notifications and Automation. Inactive Forms follow the public submission contract and are accepted.
Success returns HTTP 201 and the created Contact ID. These are persistent submissions included in Form counts. Missing or conflicting selectors, unavailable required fields/choices, and generated identity conflicts return 422 with message and errors. Authentication/IP restrictions return 401/403; throttling returns 429.
This is a write with real notifications and Automation effects. Every successful call creates a new dummy Contact; it never updates an existing Contact. The endpoint has no idempotency key. Do not blindly retry after a timeout or server failure; inspect Form submissions first. A successful submission does not guarantee completion or delivery of downstream Automation.
Input
| Parameter | Format | Required | Description |
|---|---|---|---|
form_id | Integer (body) | One selector required | Positive Form ID. Omit form_name when using this selector. |
form_name | String (body), 1–255 characters | One selector required | Full Form name. Outer whitespace is trimmed. Omit form_id when using this selector. |
Response fields
| Property | Format | Description |
|---|---|---|
success | Boolean | True when the test submission has completed. |
form_id | Integer | Selected Form ID. |
form_name | String | Selected Form name. |
contact_id | Integer | New dummy Contact ID. |
is_test | Boolean | True for this generated test submission. |
Illustrative response
{
"success": true,
"form_id": 123,
"form_name": "Request a Quote",
"contact_id": 456,
"is_test": true
}
cURL
curl 'https://your-workspace.crewvana.com/api/forms/test' \
--header 'Accept: application/json' \
--user 'YOUR_API_KEY:' \
--data 'form_id=123'
PHP
<?php
$request = curl_init('https://your-workspace.crewvana.com/api/forms/test');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => ['Accept: application/json'],
CURLOPT_USERPWD => 'YOUR_API_KEY:',
CURLOPT_POSTFIELDS => 'form_id=123',
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($request);
curl_close($request);
echo $response;
NodeJs
// Node.js 18 or later, in an ES module (.mjs). Keep API keys on the server.
const response = await fetch('https://your-workspace.crewvana.com/api/forms/test', {
method: 'POST',
headers: {
Accept: 'application/json',
Authorization: 'Basic ' + Buffer.from('YOUR_API_KEY:').toString('base64'),
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'form_id=123',
});
console.log(await response.json());
Python
# Install requests with: python -m pip install requests
import requests
response = requests.request(
'POST',
'https://your-workspace.crewvana.com/api/forms/test',
headers={'Accept': 'application/json'},
auth=('YOUR_API_KEY', ''),
data={"form_id":123},
timeout=30,
)
print(response.json())
Ruby
require 'net/http'
require 'uri'
url = URI('https://your-workspace.crewvana.com/api/forms/test')
request = Net::HTTP::Post.new(url)
request['Accept'] = 'application/json'
request.basic_auth('YOUR_API_KEY', '')
request['Content-Type'] = 'application/x-www-form-urlencoded'
request.body = 'form_id=123'
response = Net::HTTP.start(url.hostname, url.port, use_ssl: true) do |http|
http.request(request)
end
puts response.body
Read a Form
GET /api/forms/{form}
Authentication: API key (Bearer or Basic)
Read Form configuration, ordered fields, and attached Contact tags. A missing Form returns 404.
Input
| Parameter | Format | Required | Description |
|---|---|---|---|
form | Integer (path) | Required | Numeric Form ID in this workspace. |
Response fields
| Property | Format | Description |
|---|---|---|
success | Boolean | True for successful read responses. |
tenant_id | String | Workspace identifier. |
generated_at_utc | String | UTC timestamp when the detail snapshot was generated. |
form | Object | id, name, is_active, form_style_id, pipeline_id, pipeline_status_id, default_source, campaign_source, channel_source, show_name, show_description, description, success_message, redirect_url, created_at, updated_at, fields, contact_tags. The token is not returned. |
form.fields | Array | First 100 by order, then id: id, form_id, field, label, required (Boolean), order, custom_field_id, created_at, updated_at. |
form.contact_tags | Array | First 100 by name, then id: id, name, color. No truncation flags or follow-up cursors are provided for these two collections. |
Illustrative response (selected fields)
{
"success": true,
"tenant_id": "example-workspace",
"generated_at_utc": "2026-09-29T12:00:00+00:00",
"form": {
"id": 123,
"name": "Quote request",
"is_active": true,
"fields": [],
"contact_tags": []
}
}
cURL
curl 'https://your-workspace.crewvana.com/api/forms/123' \
--header 'Accept: application/json' \
--user 'YOUR_API_KEY:'
PHP
<?php
$request = curl_init('https://your-workspace.crewvana.com/api/forms/123');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => ['Accept: application/json'],
CURLOPT_USERPWD => 'YOUR_API_KEY:',
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($request);
curl_close($request);
echo $response;
NodeJs
// Node.js 18 or later, in an ES module (.mjs). Keep API keys on the server.
const response = await fetch('https://your-workspace.crewvana.com/api/forms/123', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Basic ' + Buffer.from('YOUR_API_KEY:').toString('base64'),
},
});
console.log(await response.json());
Python
# Install requests with: python -m pip install requests
import requests
response = requests.request(
'GET',
'https://your-workspace.crewvana.com/api/forms/123',
headers={'Accept': 'application/json'},
auth=('YOUR_API_KEY', ''),
timeout=30,
)
print(response.json())
Ruby
require 'net/http'
require 'uri'
url = URI('https://your-workspace.crewvana.com/api/forms/123')
request = Net::HTTP::Get.new(url)
request['Accept'] = 'application/json'
request.basic_auth('YOUR_API_KEY', '')
response = Net::HTTP.start(url.hostname, url.port, use_ssl: true) do |http|
http.request(request)
end
puts response.body
List Form submissions
GET /api/forms/{form}/submissions
Authentication: API key (Bearer or Basic)
Includes only submission activities, not every Form activity. Related Contact or communication fields may be null. A missing Form returns 404.
Input
| Parameter | Format | Required | Description |
|---|---|---|---|
form | Integer (path) | Required | Numeric Form ID. |
contact_id | Integer | Optional | Minimum 1; filter to one Contact. |
from | Date | Optional | Inclusive lower bound on occurred_at. Include a time to avoid excluding part of a day. |
to | Date | Optional | Inclusive upper bound on occurred_at; a date without a time means midnight, not the end of the day. |
limit | Integer | Optional | 1–100; default 50. |
offset | Integer | Optional | 0–5000; default 0. |
Response fields
| Property | Format | Description |
|---|---|---|
success | Boolean | True for successful read responses. |
submissions | Array | id, form_id, contact_id, opportunity_id, comm_event_id, activity_type, payload (decoded and sanitized), occurred_at, created_at, updated_at, contact_name, contact_status, contact_archived (Boolean or null), comm_conversation_id, comm_channel_id, event_direction, event_type, event_status, event_occurred_at. Sorted by occurred_at descending, then id descending. |
count | Integer | Number of rows on this page, not the total match count. |
offset | Integer | Current offset. |
next_offset | Integer or null | Pass as offset for the next page; null when no further page exists. |
has_more | Boolean | Whether more matching rows exist. |
tenant_id | String | Workspace identifier. |
form_id | Integer | Requested Form ID. |
Illustrative response
{
"success": true,
"tenant_id": "example-workspace",
"form_id": 123,
"submissions": [],
"count": 0,
"offset": 0,
"next_offset": null,
"has_more": false
}
cURL
curl 'https://your-workspace.crewvana.com/api/forms/123/submissions?limit=50' \
--header 'Accept: application/json' \
--user 'YOUR_API_KEY:'
PHP
<?php
$request = curl_init('https://your-workspace.crewvana.com/api/forms/123/submissions?limit=50');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => ['Accept: application/json'],
CURLOPT_USERPWD => 'YOUR_API_KEY:',
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($request);
curl_close($request);
echo $response;
NodeJs
// Node.js 18 or later, in an ES module (.mjs). Keep API keys on the server.
const response = await fetch('https://your-workspace.crewvana.com/api/forms/123/submissions?limit=50', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Basic ' + Buffer.from('YOUR_API_KEY:').toString('base64'),
},
});
console.log(await response.json());
Python
# Install requests with: python -m pip install requests
import requests
response = requests.request(
'GET',
'https://your-workspace.crewvana.com/api/forms/123/submissions?limit=50',
headers={'Accept': 'application/json'},
auth=('YOUR_API_KEY', ''),
timeout=30,
)
print(response.json())
Ruby
require 'net/http'
require 'uri'
url = URI('https://your-workspace.crewvana.com/api/forms/123/submissions?limit=50')
request = Net::HTTP::Get.new(url)
request['Accept'] = 'application/json'
request.basic_auth('YOUR_API_KEY', '')
response = Net::HTTP.start(url.hostname, url.port, use_ssl: true) do |http|
http.request(request)
end
puts response.body
Search Automations
GET /api/automations
Authentication: API key (Bearer or Basic)
Search by Automation name or exact numeric ID. Inactive Automations are included by default. These read routes have no explicit automation plan.feature middleware.
Input
| Parameter | Format | Required | Description |
|---|---|---|---|
query | String | Required | 1–120 characters before trimming. Search by name or numeric ID. |
limit | Integer | Optional | 1–25; default 10. |
offset | Integer | Optional | 0–1000; default 0. |
include_inactive | Boolean | Optional | Default true. Send 0 to exclude archived/inactive records; use 0 or 1 in query strings. |
Response fields
| Property | Format | Description |
|---|---|---|
success | Boolean | True for successful read responses. |
automations | Array | id, name, is_active, delay_seconds, draft_revision, published_revision, published_at, current_publication_id, has_unpublished_changes, created_at, updated_at, enrollment_count. Sorted by name, then id. is_active and has_unpublished_changes are Booleans. |
count | Integer | Number of rows on this page, not the total match count. |
offset | Integer | Current offset. |
next_offset | Integer or null | Pass as offset for the next page; null when no further page exists. |
has_more | Boolean | Whether more matching rows exist. |
Illustrative response
{
"success": true,
"automations": [],
"count": 0,
"offset": 0,
"next_offset": null,
"has_more": false
}
cURL
curl 'https://your-workspace.crewvana.com/api/automations?query=Follow&limit=10' \
--header 'Accept: application/json' \
--user 'YOUR_API_KEY:'
PHP
<?php
$request = curl_init('https://your-workspace.crewvana.com/api/automations?query=Follow&limit=10');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => ['Accept: application/json'],
CURLOPT_USERPWD => 'YOUR_API_KEY:',
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($request);
curl_close($request);
echo $response;
NodeJs
// Node.js 18 or later, in an ES module (.mjs). Keep API keys on the server.
const response = await fetch('https://your-workspace.crewvana.com/api/automations?query=Follow&limit=10', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Basic ' + Buffer.from('YOUR_API_KEY:').toString('base64'),
},
});
console.log(await response.json());
Python
# Install requests with: python -m pip install requests
import requests
response = requests.request(
'GET',
'https://your-workspace.crewvana.com/api/automations?query=Follow&limit=10',
headers={'Accept': 'application/json'},
auth=('YOUR_API_KEY', ''),
timeout=30,
)
print(response.json())
Ruby
require 'net/http'
require 'uri'
url = URI('https://your-workspace.crewvana.com/api/automations?query=Follow&limit=10')
request = Net::HTTP::Get.new(url)
request['Accept'] = 'application/json'
request.basic_auth('YOUR_API_KEY', '')
response = Net::HTTP.start(url.hostname, url.port, use_ssl: true) do |http|
http.request(request)
end
puts response.body
Read an Automation
GET /api/automations/{automation}
Authentication: API key (Bearer or Basic)
Read draft state, the current immutable publication, and bounded history. A missing Automation returns 404. This endpoint does not publish or execute an Automation.
Input
| Parameter | Format | Required | Description |
|---|---|---|---|
automation | Integer (path) | Required | Numeric Automation ID. |
Response fields
| Property | Format | Description |
|---|---|---|
success | Boolean | True for successful read responses. |
tenant_id | String | Workspace identifier. |
generated_at_utc | String | UTC timestamp when the detail snapshot was generated. |
automation | Object | id, name, is_active, delay_seconds, draft_revision, published_revision, published_at, current_publication_id, has_unpublished_changes, created_at, updated_at, automation_folder_id, published_by_user_id, is_published_for_runtime, draft_nodes, current_publication, publications, history. is_published_for_runtime is true only when active with a current publication. |
automation.draft_nodes | Array | Up to 500 nodes ordered by roots first, parent_id, sort_order, id. Fields: id, automation_id, parent_id, parent_condition, type, name, sort_order, context_id, context_type, context_sub_id, context_sub_type, custom_vars, display_json, display_name, url, json, created_at, updated_at. JSON fields are decoded/sanitized. |
automation.current_publication | Object or null | id, automation_id, revision, schema_version, content_hash, published_at, published_by_user_id, created_at, updated_at, snapshot (decoded and sanitized publication data). |
automation.publications | Array | Up to 50 publication metadata rows by revision descending; publication fields above except snapshot. Use the publications endpoint for paginated snapshots. |
automation.history | Array | Up to 100 events by created_at/id descending: id, automation_id, user_id, event_type, name, created_at, updated_at. No truncation flags are provided for draft/history collections. |
Illustrative response (selected fields)
{
"success": true,
"tenant_id": "example-workspace",
"generated_at_utc": "2026-09-29T12:00:00+00:00",
"automation": {
"id": 123,
"name": "Follow up",
"is_active": false,
"is_published_for_runtime": false,
"current_publication": null,
"draft_nodes": [],
"publications": [],
"history": []
}
}
cURL
curl 'https://your-workspace.crewvana.com/api/automations/123' \
--header 'Accept: application/json' \
--user 'YOUR_API_KEY:'
PHP
<?php
$request = curl_init('https://your-workspace.crewvana.com/api/automations/123');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => ['Accept: application/json'],
CURLOPT_USERPWD => 'YOUR_API_KEY:',
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($request);
curl_close($request);
echo $response;
NodeJs
// Node.js 18 or later, in an ES module (.mjs). Keep API keys on the server.
const response = await fetch('https://your-workspace.crewvana.com/api/automations/123', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Basic ' + Buffer.from('YOUR_API_KEY:').toString('base64'),
},
});
console.log(await response.json());
Python
# Install requests with: python -m pip install requests
import requests
response = requests.request(
'GET',
'https://your-workspace.crewvana.com/api/automations/123',
headers={'Accept': 'application/json'},
auth=('YOUR_API_KEY', ''),
timeout=30,
)
print(response.json())
Ruby
require 'net/http'
require 'uri'
url = URI('https://your-workspace.crewvana.com/api/automations/123')
request = Net::HTTP::Get.new(url)
request['Accept'] = 'application/json'
request.basic_auth('YOUR_API_KEY', '')
response = Net::HTTP.start(url.hostname, url.port, use_ssl: true) do |http|
http.request(request)
end
puts response.body
List Automation publications
GET /api/automations/{automation}/publications
Authentication: API key (Bearer or Basic)
Returns immutable publication snapshots, not live draft nodes. A missing Automation returns 404.
Input
| Parameter | Format | Required | Description |
|---|---|---|---|
automation | Integer (path) | Required | Numeric Automation ID. |
limit | Integer | Optional | 1–25; default 10. |
offset | Integer | Optional | 0–5000; default 0. |
Response fields
| Property | Format | Description |
|---|---|---|
success | Boolean | True for successful read responses. |
publications | Array | id, automation_id, revision, schema_version, content_hash, published_at, published_by_user_id, created_at, updated_at, snapshot (decoded and sanitized publication data). Sorted by revision descending. |
count | Integer | Number of rows on this page, not the total match count. |
offset | Integer | Current offset. |
next_offset | Integer or null | Pass as offset for the next page; null when no further page exists. |
has_more | Boolean | Whether more matching rows exist. |
tenant_id | String | Workspace identifier. |
automation_id | Integer | Requested Automation ID. |
Illustrative response
{
"success": true,
"tenant_id": "example-workspace",
"automation_id": 123,
"publications": [],
"count": 0,
"offset": 0,
"next_offset": null,
"has_more": false
}
cURL
curl 'https://your-workspace.crewvana.com/api/automations/123/publications?limit=10' \
--header 'Accept: application/json' \
--user 'YOUR_API_KEY:'
PHP
<?php
$request = curl_init('https://your-workspace.crewvana.com/api/automations/123/publications?limit=10');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => ['Accept: application/json'],
CURLOPT_USERPWD => 'YOUR_API_KEY:',
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($request);
curl_close($request);
echo $response;
NodeJs
// Node.js 18 or later, in an ES module (.mjs). Keep API keys on the server.
const response = await fetch('https://your-workspace.crewvana.com/api/automations/123/publications?limit=10', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Basic ' + Buffer.from('YOUR_API_KEY:').toString('base64'),
},
});
console.log(await response.json());
Python
# Install requests with: python -m pip install requests
import requests
response = requests.request(
'GET',
'https://your-workspace.crewvana.com/api/automations/123/publications?limit=10',
headers={'Accept': 'application/json'},
auth=('YOUR_API_KEY', ''),
timeout=30,
)
print(response.json())
Ruby
require 'net/http'
require 'uri'
url = URI('https://your-workspace.crewvana.com/api/automations/123/publications?limit=10')
request = Net::HTTP::Get.new(url)
request['Accept'] = 'application/json'
request.basic_auth('YOUR_API_KEY', '')
response = Net::HTTP.start(url.hostname, url.port, use_ssl: true) do |http|
http.request(request)
end
puts response.body
List Automation logs
GET /api/automations/{automation}/logs
Authentication: API key (Bearer or Basic)
Read execution log summaries. A missing Automation returns 404.
Input
| Parameter | Format | Required | Description |
|---|---|---|---|
automation | Integer (path) | Required | Numeric Automation ID. |
from | Date | Optional | Inclusive lower bound on created_at. Include a time to avoid excluding part of a day. |
to | Date | Optional | Inclusive upper bound on created_at; a date without a time means midnight, not the end of the day. |
limit | Integer | Optional | 1–100; default 50. |
offset | Integer | Optional | 0–5000; default 0. |
Response fields
| Property | Format | Description |
|---|---|---|
success | Boolean | True for successful read responses. |
logs | Array | id, automation_id, status, message (up to 4,000 characters), created_at, updated_at, enrollment_id (associated enrollment or null). Sorted by created_at descending, then id descending. |
count | Integer | Number of rows on this page, not the total match count. |
offset | Integer | Current offset. |
next_offset | Integer or null | Pass as offset for the next page; null when no further page exists. |
has_more | Boolean | Whether more matching rows exist. |
tenant_id | String | Workspace identifier. |
automation_id | Integer | Requested Automation ID. |
Illustrative response
{
"success": true,
"tenant_id": "example-workspace",
"automation_id": 123,
"logs": [],
"count": 0,
"offset": 0,
"next_offset": null,
"has_more": false
}
cURL
curl 'https://your-workspace.crewvana.com/api/automations/123/logs?limit=50' \
--header 'Accept: application/json' \
--user 'YOUR_API_KEY:'
PHP
<?php
$request = curl_init('https://your-workspace.crewvana.com/api/automations/123/logs?limit=50');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => ['Accept: application/json'],
CURLOPT_USERPWD => 'YOUR_API_KEY:',
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($request);
curl_close($request);
echo $response;
NodeJs
// Node.js 18 or later, in an ES module (.mjs). Keep API keys on the server.
const response = await fetch('https://your-workspace.crewvana.com/api/automations/123/logs?limit=50', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Basic ' + Buffer.from('YOUR_API_KEY:').toString('base64'),
},
});
console.log(await response.json());
Python
# Install requests with: python -m pip install requests
import requests
response = requests.request(
'GET',
'https://your-workspace.crewvana.com/api/automations/123/logs?limit=50',
headers={'Accept': 'application/json'},
auth=('YOUR_API_KEY', ''),
timeout=30,
)
print(response.json())
Ruby
require 'net/http'
require 'uri'
url = URI('https://your-workspace.crewvana.com/api/automations/123/logs?limit=50')
request = Net::HTTP::Get.new(url)
request['Accept'] = 'application/json'
request.basic_auth('YOUR_API_KEY', '')
response = Net::HTTP.start(url.hostname, url.port, use_ssl: true) do |http|
http.request(request)
end
puts response.body
List Automation enrollments
GET /api/automations/{automation}/enrollments
Authentication: API key (Bearer or Basic)
Read enrollments with their log, executions, and waits. This does not create, cancel, or retry enrollments. Related execution/wait rows are limited to 1,000 per collection across the entire page, ordered by enrollment ID then row ID. A missing Automation returns 404.
Input
| Parameter | Format | Required | Description |
|---|---|---|---|
automation | Integer (path) | Required | Numeric Automation ID. |
subject_type | String | Optional | One of contact, service_job, user, team. |
subject_id | Integer | Optional | Minimum 1. Use with subject_type to disambiguate IDs across object types. |
from | Date | Optional | Inclusive lower bound on enrolled_at. Include a time to avoid excluding part of a day. |
to | Date | Optional | Inclusive upper bound on enrolled_at; a date without a time means midnight, not the end of the day. |
limit | Integer | Optional | 1–50; default 25. |
offset | Integer | Optional | 0–5000; default 0. |
Response fields
| Property | Format | Description |
|---|---|---|
success | Boolean | True for successful read responses. |
enrollments | Array | id, automation_id, automation_log_id, automation_publication_id, runtime_version, subject_type, subject_id, subject_label, context (sanitized), enrolled_at, created_at, updated_at, log (object or null), executions, waits. Sorted by enrolled_at descending, then id descending. |
count | Integer | Number of rows on this page, not the total match count. |
offset | Integer | Current offset. |
next_offset | Integer or null | Pass as offset for the next page; null when no further page exists. |
has_more | Boolean | Whether more matching rows exist. |
tenant_id | String | Workspace identifier. |
automation_id | Integer | Requested Automation ID. |
enrollments[].log | Object or null | id, automation_id, status, message (4,000 characters), created_at, updated_at. |
enrollments[].executions | Array | id, automation_id, automation_node_id, automation_log_id, automation_enrollment_id, automation_publication_id, publication_node_key, occurrence_key, continuation_state (decoded/sanitized), status, action_name, subject_type, subject_id, subject_label, result (4,000 characters), executed_at, created_at, updated_at. |
enrollments[].waits | Array | id, automation_id, automation_node_id, automation_enrollment_id, automation_execution_id, automation_publication_id, publication_node_key, continuation_branch, next_sort_order, status, mode, settings, context (decoded/sanitized), due_at, timeout_at, satisfied_at, satisfied_branch, failure_message (4,000 characters), claimed_at, claim_expires_at, created_at, updated_at. |
executions_has_more | Boolean | More than 1,000 related execution rows exist across this page; no separate related-row cursor is exposed. |
waits_has_more | Boolean | More than 1,000 related wait rows exist across this page; no separate related-row cursor is exposed. |
Illustrative response
{
"success": true,
"tenant_id": "example-workspace",
"automation_id": 123,
"enrollments": [],
"count": 0,
"offset": 0,
"next_offset": null,
"has_more": false,
"executions_has_more": false,
"waits_has_more": false
}
cURL
curl 'https://your-workspace.crewvana.com/api/automations/123/enrollments?limit=25' \
--header 'Accept: application/json' \
--user 'YOUR_API_KEY:'
PHP
<?php
$request = curl_init('https://your-workspace.crewvana.com/api/automations/123/enrollments?limit=25');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => ['Accept: application/json'],
CURLOPT_USERPWD => 'YOUR_API_KEY:',
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($request);
curl_close($request);
echo $response;
NodeJs
// Node.js 18 or later, in an ES module (.mjs). Keep API keys on the server.
const response = await fetch('https://your-workspace.crewvana.com/api/automations/123/enrollments?limit=25', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Basic ' + Buffer.from('YOUR_API_KEY:').toString('base64'),
},
});
console.log(await response.json());
Python
# Install requests with: python -m pip install requests
import requests
response = requests.request(
'GET',
'https://your-workspace.crewvana.com/api/automations/123/enrollments?limit=25',
headers={'Accept': 'application/json'},
auth=('YOUR_API_KEY', ''),
timeout=30,
)
print(response.json())
Ruby
require 'net/http'
require 'uri'
url = URI('https://your-workspace.crewvana.com/api/automations/123/enrollments?limit=25')
request = Net::HTTP::Get.new(url)
request['Accept'] = 'application/json'
request.basic_auth('YOUR_API_KEY', '')
response = Net::HTTP.start(url.hostname, url.port, use_ssl: true) do |http|
http.request(request)
end
puts response.body
Trigger an automation (legacy)
POST /api/automation/trigger
Authentication: Legacy plan gate; read API key middleware absent · Plan feature: automation
Requires the automation plan feature. This legacy route does not use the read-route key/IP authentication middleware or explicit read-route throttle. It is not recommended for new integrations until its access controls and contract are aligned.
The current handler dispatches a Webhook trigger with optional Contact context. The required name is not consumed by the handler; a success response confirms dispatch, not workflow completion. It does not select a named Automation.
Use application/x-www-form-urlencoded, not JSON: the legacy request reader consumes form/query variables. Unknown input parameters can return 422. Supplying an unknown Contact integration link fails; no Contact is created.
Input
| Parameter | Format | Required | Description |
|---|---|---|---|
name | String | Required | Required by legacy validation, but currently not used to select/filter an Automation. |
contact | Object (form fields) | Optional | Send contact[integration] and contact[external_id] to identify a Contact through its existing integration link. |
Response fields
| Property | Format | Description |
|---|---|---|
success | Boolean | True when a successful legacy dispatch response is wrapped. |
message | String | Webhook trigger dispatched |
Illustrative response
{
"message": "Webhook trigger dispatched",
"success": true
}
cURL
curl 'https://your-workspace.crewvana.com/api/automation/trigger' \
--header 'Accept: application/json' \
--user 'YOUR_API_KEY:' \
--data 'name=YOUR_TRIGGER_NAME'
PHP
<?php
$request = curl_init('https://your-workspace.crewvana.com/api/automation/trigger');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => ['Accept: application/json'],
CURLOPT_USERPWD => 'YOUR_API_KEY:',
CURLOPT_POSTFIELDS => 'name=YOUR_TRIGGER_NAME',
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($request);
curl_close($request);
echo $response;
NodeJs
// Node.js 18 or later, in an ES module (.mjs). Keep API keys on the server.
const response = await fetch('https://your-workspace.crewvana.com/api/automation/trigger', {
method: 'POST',
headers: {
Accept: 'application/json',
Authorization: 'Basic ' + Buffer.from('YOUR_API_KEY:').toString('base64'),
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'name=YOUR_TRIGGER_NAME',
});
console.log(await response.json());
Python
# Install requests with: python -m pip install requests
import requests
response = requests.request(
'POST',
'https://your-workspace.crewvana.com/api/automation/trigger',
headers={'Accept': 'application/json'},
auth=('YOUR_API_KEY', ''),
data={"name":"YOUR_TRIGGER_NAME"},
timeout=30,
)
print(response.json())
Ruby
require 'net/http'
require 'uri'
url = URI('https://your-workspace.crewvana.com/api/automation/trigger')
request = Net::HTTP::Post.new(url)
request['Accept'] = 'application/json'
request.basic_auth('YOUR_API_KEY', '')
request['Content-Type'] = 'application/x-www-form-urlencoded'
request.body = 'name=YOUR_TRIGGER_NAME'
response = Net::HTTP.start(url.hostname, url.port, use_ssl: true) do |http|
http.request(request)
end
puts response.body
Obsolete Contact pipeline endpoint
POST /api/contact/move-in-pipeline
Authentication: Legacy plan gate; read API key middleware absent · Plan feature: pipeline
Obsolete: Opportunities, not Contacts, belong in pipelines. Do not use this endpoint for new integrations. It remains listed only because its legacy URL is still registered. No replacement Opportunity movement endpoint is currently registered in the tenant integration API.
Requires the pipeline plan feature. The handler is a placeholder: it sleeps for one second and returns a string without performing a pipeline mutation. The legacy response wrapper cannot turn that string into the declared message output and can raise a server error.
The fields below describe legacy request metadata only, not a supported movement contract. Declared integer formats are not Laravel integer validation rules. The legacy package reads form/query inputs.
This legacy route does not use the read-route key/IP authentication middleware or explicit read-route throttle. Its presence in this inventory is not a guarantee of usable write functionality.
Input
| Parameter | Format | Required | Description |
|---|---|---|---|
contact_id | Integer | Required | Contact ID declared by the legacy request metadata. |
pipeline_id | Integer | Required | Pipeline ID declared by the legacy request metadata. |
status_id | Integer | Required | Stage ID declared by the legacy request metadata. |
Response fields
| Property | Format | Description |
|---|---|---|
Successful move response | Unavailable | No supported successful mutation contract exists for the current placeholder handler. |
Service resource writes and input contracts
All resource routes require the tenant key/IP middleware and share the 60-per-minute throttle. Writes additionally require JSON, actor_user_id, and Idempotency-Key. The key grants integration access; a selected active User supplies permission checks and attribution, not proof of that User’s login. Never select another actor merely to bypass permission denial.
Job creation requires calendar.create. Job/Visit changes require calendar.update plus service_jobs.update for an assigned Visit or service_jobs.update_all. Job deletion requires calendar.delete. Subscription management requires payments.update or Job create permission. Pending migration api_mutation_receipt must be applied before writes can run.
Job lines support item/package/discount with the corresponding price_book_*_id, positive integer quantity, optional price (item/package) or amount (discount), and existing line_item_id for an update. At most 100 ordered lines. New packages/discounts require active catalog entries; existing matching snapshots can retain retired entries. Fixed-price items retain authoritative catalog/saved prices. API commercial edits are blocked when execution or financial history exists.
Job create accepts at most 25 ordered Visits. Visits accept nullable scheduled_start/end formatted YYYY-MM-DD HH:MM:SS in business-local wall time, technician_ids (up to 25 unique active schedulable Users), and allow_block_overlap. Scheduling overlaps with Blocks require an explicit override. Read timestamps are saved values; legacy manual and generated records may use different storage conventions, so do not infer a UTC offset from a bare timestamp.
Subscription PUT replaces the complete definition using its current lock_version; it does not rewrite generated history. Required: contact_id, active price_book_subscription_id, timezone, effective_start_date, initial_visit_date (YYYY-MM-DD), generation_horizon_days (1–730), billing_terms (per-service/monthly/upfront), recurrence and job_templates. Optional: opportunity_id, effective_end_date, occurrence_limit (1–100000), apply_tax, total_due_per_service, short_month_policy (clamp_to_last_day/skip), dst_gap_policy (conflict/shift_forward), dst_fold_policy (earlier/later), week_starts_on (0–6).
Recurrence requires frequency (day/week/month/year) and local_time (HH:MM:SS); interval defaults to 1. Optional weekly_days (0–6), monthly_mode (day_of_month/nth_weekday), day_of_month, week_of_month, day_of_week, annual_month and annual_day follow canonical recurrence validation. Each of up to 25 Job templates requires name, 1–100 lines and 1–25 Visits. Optional stable_key, notes, cycle_offset_days and apply_tax preserve template identity and defaults.
Subscription lines require line_type (item/package/discount/custom), name, positive quantity and nonnegative amount; catalog IDs and duration_minutes are optional. Visit templates require name, duration_minutes (1–10080) and preference_mode (exact/window/none). Optional stable_key, offset_days, preferred_start_time/end_time and preferred_technicians [{user_id, preference_mode: preferred|required}] define dispatch intent. Unknown input fields and arbitrary metadata/default snapshots are rejected. Definition template data is bounded; preserve stable keys when revising an existing agreement.
Successful create returns 201; other writes return 200 with success, tenant_id and resource ID. Subscription writes also return lock_version, current_version_number and lifecycle status. Failed writes roll back; only successes get receipts. Use a new key for a changed request. Receipts are tenant-local, keyed by API-key identity, and retained to prevent later retries from duplicating resources. They do not reconstruct historical requests or device errors.
List customer Subscriptions
GET /api/subscriptions
Authentication: API key and IP allowlist
Optional filters: contact_id, price_book_subscription_id, and exact lifecycle status. Returns subscriptions.items with lock_version, plan summary and pagination.
Input
| Parameter | Format | Required | Description |
|---|---|---|---|
limit | Integer | Optional | 1–100, default 25. |
offset | Integer | Optional | 0–10000, default 0. |
cURL
curl 'https://your-workspace.crewvana.com/api/subscriptions?limit=25' \
--header 'Accept: application/json' \
--user 'YOUR_API_KEY:'
PHP
<?php
$request = curl_init('https://your-workspace.crewvana.com/api/subscriptions?limit=25');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => ['Accept: application/json'],
CURLOPT_USERPWD => 'YOUR_API_KEY:',
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($request);
curl_close($request);
echo $response;
NodeJs
// Node.js 18 or later, in an ES module (.mjs). Keep API keys on the server.
const response = await fetch('https://your-workspace.crewvana.com/api/subscriptions?limit=25', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Basic ' + Buffer.from('YOUR_API_KEY:').toString('base64'),
},
});
console.log(await response.json());
Python
# Install requests with: python -m pip install requests
import requests
response = requests.request(
'GET',
'https://your-workspace.crewvana.com/api/subscriptions?limit=25',
headers={'Accept': 'application/json'},
auth=('YOUR_API_KEY', ''),
timeout=30,
)
print(response.json())
Ruby
require 'net/http'
require 'uri'
url = URI('https://your-workspace.crewvana.com/api/subscriptions?limit=25')
request = Net::HTTP::Get.new(url)
request['Accept'] = 'application/json'
request.basic_auth('YOUR_API_KEY', '')
response = Net::HTTP.start(url.hostname, url.port, use_ssl: true) do |http|
http.request(request)
end
puts response.body
Create a Subscription draft
POST /api/subscriptions
Authentication: API key and IP allowlist
Creates a draft and immutable definition version; does not generate Jobs. Definition fields and limits are listed below.
Input
| Parameter | Format | Required | Description |
|---|---|---|---|
actor_user_id | Integer (JSON body) | Required | Active workspace User whose application permissions authorize the operation. API-key identity is recorded separately. |
Idempotency-Key | HTTP header | Required | 1–100 ASCII letters, digits, dot, underscore, colon or hyphen. Reuse for the same request only. A different method/path/body returns 409; successful replay returns the original status/body and Idempotency-Replayed: true. |
cURL
curl 'https://your-workspace.crewvana.com/api/subscriptions' \
--request POST \
--user 'YOUR_API_KEY:' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: replace-with-a-unique-request-id' \
--data '{"actor_user_id":2,"contact_id":42,"price_book_subscription_id":2,"timezone":"America/Denver","effective_start_date":"2026-10-05","initial_visit_date":"2026-10-05","generation_horizon_days":90,"billing_terms":"per-service","occurrence_limit":1,"recurrence":{"frequency":"month","interval":1,"local_time":"10:00:00","day_of_month":5},"job_templates":[{"name":"Service","lines":[{"line_type":"custom","name":"Service","quantity":1,"amount":100}],"visits":[{"name":"Visit","duration_minutes":60,"preference_mode":"exact","preferred_start_time":"10:00:00","preferred_technicians":[{"user_id":2,"preference_mode":"preferred"}]}]}]}'
PHP
<?php
$request = curl_init('https://your-workspace.crewvana.com/api/subscriptions');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_USERPWD => 'YOUR_API_KEY:',
CURLOPT_HTTPHEADER => ['Accept: application/json', 'Content-Type: application/json', 'Idempotency-Key: replace-with-a-unique-request-id'],
CURLOPT_POSTFIELDS => '{"actor_user_id":2,"contact_id":42,"price_book_subscription_id":2,"timezone":"America/Denver","effective_start_date":"2026-10-05","initial_visit_date":"2026-10-05","generation_horizon_days":90,"billing_terms":"per-service","occurrence_limit":1,"recurrence":{"frequency":"month","interval":1,"local_time":"10:00:00","day_of_month":5},"job_templates":[{"name":"Service","lines":[{"line_type":"custom","name":"Service","quantity":1,"amount":100}],"visits":[{"name":"Visit","duration_minutes":60,"preference_mode":"exact","preferred_start_time":"10:00:00","preferred_technicians":[{"user_id":2,"preference_mode":"preferred"}]}]}]}',
CURLOPT_RETURNTRANSFER => true,
]);
echo curl_exec($request);
curl_close($request);
NodeJs
// Run on the server; keep API keys out of browser code.
const response = await fetch('https://your-workspace.crewvana.com/api/subscriptions', {
method: 'POST',
headers: {
Authorization: 'Bearer YOUR_API_KEY',
Accept: 'application/json',
'Content-Type': 'application/json',
'Idempotency-Key': 'replace-with-a-unique-request-id',
},
body: JSON.stringify({"actor_user_id":2,"contact_id":42,"price_book_subscription_id":2,"timezone":"America/Denver","effective_start_date":"2026-10-05","initial_visit_date":"2026-10-05","generation_horizon_days":90,"billing_terms":"per-service","occurrence_limit":1,"recurrence":{"frequency":"month","interval":1,"local_time":"10:00:00","day_of_month":5},"job_templates":[{"name":"Service","lines":[{"line_type":"custom","name":"Service","quantity":1,"amount":100}],"visits":[{"name":"Visit","duration_minutes":60,"preference_mode":"exact","preferred_start_time":"10:00:00","preferred_technicians":[{"user_id":2,"preference_mode":"preferred"}]}]}]}),
});
console.log(await response.json());
Python
import json
import requests
response = requests.request(
'POST',
'https://your-workspace.crewvana.com/api/subscriptions',
auth=('YOUR_API_KEY', ''),
headers={'Accept': 'application/json', 'Idempotency-Key': 'replace-with-a-unique-request-id'},
json=json.loads("{\"actor_user_id\":2,\"contact_id\":42,\"price_book_subscription_id\":2,\"timezone\":\"America/Denver\",\"effective_start_date\":\"2026-10-05\",\"initial_visit_date\":\"2026-10-05\",\"generation_horizon_days\":90,\"billing_terms\":\"per-service\",\"occurrence_limit\":1,\"recurrence\":{\"frequency\":\"month\",\"interval\":1,\"local_time\":\"10:00:00\",\"day_of_month\":5},\"job_templates\":[{\"name\":\"Service\",\"lines\":[{\"line_type\":\"custom\",\"name\":\"Service\",\"quantity\":1,\"amount\":100}],\"visits\":[{\"name\":\"Visit\",\"duration_minutes\":60,\"preference_mode\":\"exact\",\"preferred_start_time\":\"10:00:00\",\"preferred_technicians\":[{\"user_id\":2,\"preference_mode\":\"preferred\"}]}]}]}"),
timeout=30,
)
print(response.json())
Ruby
require 'net/http'
require 'uri'
url = URI('https://your-workspace.crewvana.com/api/subscriptions')
request = Net::HTTP::Post.new(url)
request.basic_auth('YOUR_API_KEY', '')
request['Accept'] = 'application/json'
request['Content-Type'] = 'application/json'
request['Idempotency-Key'] = 'replace-with-a-unique-request-id'
request.body = "{\"actor_user_id\":2,\"contact_id\":42,\"price_book_subscription_id\":2,\"timezone\":\"America/Denver\",\"effective_start_date\":\"2026-10-05\",\"initial_visit_date\":\"2026-10-05\",\"generation_horizon_days\":90,\"billing_terms\":\"per-service\",\"occurrence_limit\":1,\"recurrence\":{\"frequency\":\"month\",\"interval\":1,\"local_time\":\"10:00:00\",\"day_of_month\":5},\"job_templates\":[{\"name\":\"Service\",\"lines\":[{\"line_type\":\"custom\",\"name\":\"Service\",\"quantity\":1,\"amount\":100}],\"visits\":[{\"name\":\"Visit\",\"duration_minutes\":60,\"preference_mode\":\"exact\",\"preferred_start_time\":\"10:00:00\",\"preferred_technicians\":[{\"user_id\":2,\"preference_mode\":\"preferred\"}]}]}]}"
response = Net::HTTP.start(url.hostname, url.port, use_ssl: true) { |http| http.request(request) }
puts response.body
Use a fresh request ID for each intended write. Retain the same ID and body when retrying.
Replace a Subscription definition
PUT /api/subscriptions/{subscription}
Authentication: API key and IP allowlist
Supply a complete definition. Contact is immutable. Creates a new version; existing generated Jobs, Visits and protected history are retained. Cancelled/archived definitions cannot be edited.
Input
| Parameter | Format | Required | Description |
|---|---|---|---|
actor_user_id | Integer (JSON body) | Required | Active workspace User whose application permissions authorize the operation. API-key identity is recorded separately. |
Idempotency-Key | HTTP header | Required | 1–100 ASCII letters, digits, dot, underscore, colon or hyphen. Reuse for the same request only. A different method/path/body returns 409; successful replay returns the original status/body and Idempotency-Replayed: true. |
lock_version | Integer | Required | Copy from GET /api/subscriptions/{subscription}. |
cURL
curl 'https://your-workspace.crewvana.com/api/subscriptions/10' \
--request PUT \
--user 'YOUR_API_KEY:' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: replace-with-a-unique-request-id' \
--data '{"actor_user_id":2,"contact_id":42,"price_book_subscription_id":2,"timezone":"America/Denver","effective_start_date":"2026-10-05","initial_visit_date":"2026-10-05","generation_horizon_days":90,"billing_terms":"per-service","occurrence_limit":1,"recurrence":{"frequency":"month","interval":1,"local_time":"10:00:00","day_of_month":5},"job_templates":[{"name":"Service","lines":[{"line_type":"custom","name":"Service","quantity":1,"amount":100}],"visits":[{"name":"Visit","duration_minutes":60,"preference_mode":"exact","preferred_start_time":"10:00:00","preferred_technicians":[{"user_id":2,"preference_mode":"preferred"}]}]}],"lock_version":0}'
PHP
<?php
$request = curl_init('https://your-workspace.crewvana.com/api/subscriptions/10');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'PUT',
CURLOPT_USERPWD => 'YOUR_API_KEY:',
CURLOPT_HTTPHEADER => ['Accept: application/json', 'Content-Type: application/json', 'Idempotency-Key: replace-with-a-unique-request-id'],
CURLOPT_POSTFIELDS => '{"actor_user_id":2,"contact_id":42,"price_book_subscription_id":2,"timezone":"America/Denver","effective_start_date":"2026-10-05","initial_visit_date":"2026-10-05","generation_horizon_days":90,"billing_terms":"per-service","occurrence_limit":1,"recurrence":{"frequency":"month","interval":1,"local_time":"10:00:00","day_of_month":5},"job_templates":[{"name":"Service","lines":[{"line_type":"custom","name":"Service","quantity":1,"amount":100}],"visits":[{"name":"Visit","duration_minutes":60,"preference_mode":"exact","preferred_start_time":"10:00:00","preferred_technicians":[{"user_id":2,"preference_mode":"preferred"}]}]}],"lock_version":0}',
CURLOPT_RETURNTRANSFER => true,
]);
echo curl_exec($request);
curl_close($request);
NodeJs
// Run on the server; keep API keys out of browser code.
const response = await fetch('https://your-workspace.crewvana.com/api/subscriptions/10', {
method: 'PUT',
headers: {
Authorization: 'Bearer YOUR_API_KEY',
Accept: 'application/json',
'Content-Type': 'application/json',
'Idempotency-Key': 'replace-with-a-unique-request-id',
},
body: JSON.stringify({"actor_user_id":2,"contact_id":42,"price_book_subscription_id":2,"timezone":"America/Denver","effective_start_date":"2026-10-05","initial_visit_date":"2026-10-05","generation_horizon_days":90,"billing_terms":"per-service","occurrence_limit":1,"recurrence":{"frequency":"month","interval":1,"local_time":"10:00:00","day_of_month":5},"job_templates":[{"name":"Service","lines":[{"line_type":"custom","name":"Service","quantity":1,"amount":100}],"visits":[{"name":"Visit","duration_minutes":60,"preference_mode":"exact","preferred_start_time":"10:00:00","preferred_technicians":[{"user_id":2,"preference_mode":"preferred"}]}]}],"lock_version":0}),
});
console.log(await response.json());
Python
import json
import requests
response = requests.request(
'PUT',
'https://your-workspace.crewvana.com/api/subscriptions/10',
auth=('YOUR_API_KEY', ''),
headers={'Accept': 'application/json', 'Idempotency-Key': 'replace-with-a-unique-request-id'},
json=json.loads("{\"actor_user_id\":2,\"contact_id\":42,\"price_book_subscription_id\":2,\"timezone\":\"America/Denver\",\"effective_start_date\":\"2026-10-05\",\"initial_visit_date\":\"2026-10-05\",\"generation_horizon_days\":90,\"billing_terms\":\"per-service\",\"occurrence_limit\":1,\"recurrence\":{\"frequency\":\"month\",\"interval\":1,\"local_time\":\"10:00:00\",\"day_of_month\":5},\"job_templates\":[{\"name\":\"Service\",\"lines\":[{\"line_type\":\"custom\",\"name\":\"Service\",\"quantity\":1,\"amount\":100}],\"visits\":[{\"name\":\"Visit\",\"duration_minutes\":60,\"preference_mode\":\"exact\",\"preferred_start_time\":\"10:00:00\",\"preferred_technicians\":[{\"user_id\":2,\"preference_mode\":\"preferred\"}]}]}],\"lock_version\":0}"),
timeout=30,
)
print(response.json())
Ruby
require 'net/http'
require 'uri'
url = URI('https://your-workspace.crewvana.com/api/subscriptions/10')
request = Net::HTTP::Put.new(url)
request.basic_auth('YOUR_API_KEY', '')
request['Accept'] = 'application/json'
request['Content-Type'] = 'application/json'
request['Idempotency-Key'] = 'replace-with-a-unique-request-id'
request.body = "{\"actor_user_id\":2,\"contact_id\":42,\"price_book_subscription_id\":2,\"timezone\":\"America/Denver\",\"effective_start_date\":\"2026-10-05\",\"initial_visit_date\":\"2026-10-05\",\"generation_horizon_days\":90,\"billing_terms\":\"per-service\",\"occurrence_limit\":1,\"recurrence\":{\"frequency\":\"month\",\"interval\":1,\"local_time\":\"10:00:00\",\"day_of_month\":5},\"job_templates\":[{\"name\":\"Service\",\"lines\":[{\"line_type\":\"custom\",\"name\":\"Service\",\"quantity\":1,\"amount\":100}],\"visits\":[{\"name\":\"Visit\",\"duration_minutes\":60,\"preference_mode\":\"exact\",\"preferred_start_time\":\"10:00:00\",\"preferred_technicians\":[{\"user_id\":2,\"preference_mode\":\"preferred\"}]}]}],\"lock_version\":0}"
response = Net::HTTP.start(url.hostname, url.port, use_ssl: true) { |http| http.request(request) }
puts response.body
Use a fresh request ID for each intended write. Retain the same ID and body when retrying.
Delete a Subscription draft
DELETE /api/subscriptions/{subscription}
Authentication: API key and IP allowlist
Only never-activated draft/quoted agreements without Jobs, occurrences, exceptions or source Quotes can be deleted. Otherwise use cancel/archive.
Input
| Parameter | Format | Required | Description |
|---|---|---|---|
actor_user_id | Integer (JSON body) | Required | Active workspace User whose application permissions authorize the operation. API-key identity is recorded separately. |
Idempotency-Key | HTTP header | Required | 1–100 ASCII letters, digits, dot, underscore, colon or hyphen. Reuse for the same request only. A different method/path/body returns 409; successful replay returns the original status/body and Idempotency-Replayed: true. |
lock_version | Integer | Required | Current optimistic lock version. |
cURL
curl 'https://your-workspace.crewvana.com/api/subscriptions/10' \
--request DELETE \
--user 'YOUR_API_KEY:' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: replace-with-a-unique-request-id' \
--data '{"actor_user_id":2,"lock_version":0}'
PHP
<?php
$request = curl_init('https://your-workspace.crewvana.com/api/subscriptions/10');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'DELETE',
CURLOPT_USERPWD => 'YOUR_API_KEY:',
CURLOPT_HTTPHEADER => ['Accept: application/json', 'Content-Type: application/json', 'Idempotency-Key: replace-with-a-unique-request-id'],
CURLOPT_POSTFIELDS => '{"actor_user_id":2,"lock_version":0}',
CURLOPT_RETURNTRANSFER => true,
]);
echo curl_exec($request);
curl_close($request);
NodeJs
// Run on the server; keep API keys out of browser code.
const response = await fetch('https://your-workspace.crewvana.com/api/subscriptions/10', {
method: 'DELETE',
headers: {
Authorization: 'Bearer YOUR_API_KEY',
Accept: 'application/json',
'Content-Type': 'application/json',
'Idempotency-Key': 'replace-with-a-unique-request-id',
},
body: JSON.stringify({"actor_user_id":2,"lock_version":0}),
});
console.log(await response.json());
Python
import json
import requests
response = requests.request(
'DELETE',
'https://your-workspace.crewvana.com/api/subscriptions/10',
auth=('YOUR_API_KEY', ''),
headers={'Accept': 'application/json', 'Idempotency-Key': 'replace-with-a-unique-request-id'},
json=json.loads("{\"actor_user_id\":2,\"lock_version\":0}"),
timeout=30,
)
print(response.json())
Ruby
require 'net/http'
require 'uri'
url = URI('https://your-workspace.crewvana.com/api/subscriptions/10')
request = Net::HTTP::Delete.new(url)
request.basic_auth('YOUR_API_KEY', '')
request['Accept'] = 'application/json'
request['Content-Type'] = 'application/json'
request['Idempotency-Key'] = 'replace-with-a-unique-request-id'
request.body = "{\"actor_user_id\":2,\"lock_version\":0}"
response = Net::HTTP.start(url.hostname, url.port, use_ssl: true) { |http| http.request(request) }
puts response.body
Use a fresh request ID for each intended write. Retain the same ID and body when retrying.
Change Subscription lifecycle
POST /api/subscriptions/{subscription}/lifecycle
Authentication: API key and IP allowlist
Canonical lifecycle transitions apply. Activation/resume changes status only; the normal scheduler generates work later. Legacy unsafe definitions require review. Cancel stops future generation and retains existing Jobs. Archive requires cancelled/ended state.
Input
| Parameter | Format | Required | Description |
|---|---|---|---|
actor_user_id | Integer (JSON body) | Required | Active workspace User whose application permissions authorize the operation. API-key identity is recorded separately. |
Idempotency-Key | HTTP header | Required | 1–100 ASCII letters, digits, dot, underscore, colon or hyphen. Reuse for the same request only. A different method/path/body returns 409; successful replay returns the original status/body and Idempotency-Replayed: true. |
action | String | Required | activate, pause, resume, cancel, archive |
lock_version | Integer | Required | Current lock version. |
reason | String, max 1000 | Required for cancel | Nonblank cancellation reason. |
cURL
curl 'https://your-workspace.crewvana.com/api/subscriptions/10/lifecycle' \
--request POST \
--user 'YOUR_API_KEY:' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: replace-with-a-unique-request-id' \
--data '{"actor_user_id":2,"action":"pause","lock_version":1}'
PHP
<?php
$request = curl_init('https://your-workspace.crewvana.com/api/subscriptions/10/lifecycle');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_USERPWD => 'YOUR_API_KEY:',
CURLOPT_HTTPHEADER => ['Accept: application/json', 'Content-Type: application/json', 'Idempotency-Key: replace-with-a-unique-request-id'],
CURLOPT_POSTFIELDS => '{"actor_user_id":2,"action":"pause","lock_version":1}',
CURLOPT_RETURNTRANSFER => true,
]);
echo curl_exec($request);
curl_close($request);
NodeJs
// Run on the server; keep API keys out of browser code.
const response = await fetch('https://your-workspace.crewvana.com/api/subscriptions/10/lifecycle', {
method: 'POST',
headers: {
Authorization: 'Bearer YOUR_API_KEY',
Accept: 'application/json',
'Content-Type': 'application/json',
'Idempotency-Key': 'replace-with-a-unique-request-id',
},
body: JSON.stringify({"actor_user_id":2,"action":"pause","lock_version":1}),
});
console.log(await response.json());
Python
import json
import requests
response = requests.request(
'POST',
'https://your-workspace.crewvana.com/api/subscriptions/10/lifecycle',
auth=('YOUR_API_KEY', ''),
headers={'Accept': 'application/json', 'Idempotency-Key': 'replace-with-a-unique-request-id'},
json=json.loads("{\"actor_user_id\":2,\"action\":\"pause\",\"lock_version\":1}"),
timeout=30,
)
print(response.json())
Ruby
require 'net/http'
require 'uri'
url = URI('https://your-workspace.crewvana.com/api/subscriptions/10/lifecycle')
request = Net::HTTP::Post.new(url)
request.basic_auth('YOUR_API_KEY', '')
request['Accept'] = 'application/json'
request['Content-Type'] = 'application/json'
request['Idempotency-Key'] = 'replace-with-a-unique-request-id'
request.body = "{\"actor_user_id\":2,\"action\":\"pause\",\"lock_version\":1}"
response = Net::HTTP.start(url.hostname, url.port, use_ssl: true) { |http| http.request(request) }
puts response.body
Use a fresh request ID for each intended write. Retain the same ID and body when retrying.
List Jobs
GET /api/jobs
Authentication: API key and IP allowlist
Optional exact filters: contact_id, contact_subscription_id, status, paid_status (unpaid/partial/paid). Results contain bounded Visit summaries with technician check-ins.
Input
| Parameter | Format | Required | Description |
|---|---|---|---|
limit | Integer | Optional | 1–100, default 25. |
offset | Integer | Optional | 0–10000, default 0. |
cURL
curl 'https://your-workspace.crewvana.com/api/jobs?limit=25' \
--header 'Accept: application/json' \
--user 'YOUR_API_KEY:'
PHP
<?php
$request = curl_init('https://your-workspace.crewvana.com/api/jobs?limit=25');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => ['Accept: application/json'],
CURLOPT_USERPWD => 'YOUR_API_KEY:',
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($request);
curl_close($request);
echo $response;
NodeJs
// Node.js 18 or later, in an ES module (.mjs). Keep API keys on the server.
const response = await fetch('https://your-workspace.crewvana.com/api/jobs?limit=25', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Basic ' + Buffer.from('YOUR_API_KEY:').toString('base64'),
},
});
console.log(await response.json());
Python
# Install requests with: python -m pip install requests
import requests
response = requests.request(
'GET',
'https://your-workspace.crewvana.com/api/jobs?limit=25',
headers={'Accept': 'application/json'},
auth=('YOUR_API_KEY', ''),
timeout=30,
)
print(response.json())
Ruby
require 'net/http'
require 'uri'
url = URI('https://your-workspace.crewvana.com/api/jobs?limit=25')
request = Net::HTTP::Get.new(url)
request['Accept'] = 'application/json'
request.basic_auth('YOUR_API_KEY', '')
response = Net::HTTP.start(url.hostname, url.port, use_ssl: true) do |http|
http.request(request)
end
puts response.body
List a Contact’s Jobs
GET /api/contacts/{contact}/jobs
Authentication: API key and IP allowlist
Same filters, fields and bounds as /api/jobs, constrained to the path Contact. Unknown Contact returns 404.
Input
| Parameter | Format | Required | Description |
|---|---|---|---|
limit | Integer | Optional | 1–100, default 25. |
offset | Integer | Optional | 0–10000, default 0. |
cURL
curl 'https://your-workspace.crewvana.com/api/contacts/42/jobs?limit=25' \
--header 'Accept: application/json' \
--user 'YOUR_API_KEY:'
PHP
<?php
$request = curl_init('https://your-workspace.crewvana.com/api/contacts/42/jobs?limit=25');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => ['Accept: application/json'],
CURLOPT_USERPWD => 'YOUR_API_KEY:',
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($request);
curl_close($request);
echo $response;
NodeJs
// Node.js 18 or later, in an ES module (.mjs). Keep API keys on the server.
const response = await fetch('https://your-workspace.crewvana.com/api/contacts/42/jobs?limit=25', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Basic ' + Buffer.from('YOUR_API_KEY:').toString('base64'),
},
});
console.log(await response.json());
Python
# Install requests with: python -m pip install requests
import requests
response = requests.request(
'GET',
'https://your-workspace.crewvana.com/api/contacts/42/jobs?limit=25',
headers={'Accept': 'application/json'},
auth=('YOUR_API_KEY', ''),
timeout=30,
)
print(response.json())
Ruby
require 'net/http'
require 'uri'
url = URI('https://your-workspace.crewvana.com/api/contacts/42/jobs?limit=25')
request = Net::HTTP::Get.new(url)
request['Accept'] = 'application/json'
request.basic_auth('YOUR_API_KEY', '')
response = Net::HTTP.start(url.hostname, url.port, use_ssl: true) do |http|
http.request(request)
end
puts response.body
Read a Job and execution records
GET /api/jobs/{job}
Authentication: API key and IP allowlist
Returns job, catalog-named line_items, Visits and technician check-in/out, Subscription occurrence, Invoice IDs, media metadata and message logs. No payment tokens, provider payloads, storage paths or API credentials. Media/logs/technicians are capped at 100 and report truncation; use /api/visits/{visit} for checklist and line execution detail.
Input
| Parameter | Format | Required | Description |
|---|---|---|---|
line_limit / visit_limit | Integer | Optional | 1–100; defaults 100 and 25. |
line_offset / visit_offset | Integer | Optional | 0–10000; continue each nested page independently. |
cURL
curl 'https://your-workspace.crewvana.com/api/jobs/100' \
--header 'Accept: application/json' \
--user 'YOUR_API_KEY:'
PHP
<?php
$request = curl_init('https://your-workspace.crewvana.com/api/jobs/100');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => ['Accept: application/json'],
CURLOPT_USERPWD => 'YOUR_API_KEY:',
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($request);
curl_close($request);
echo $response;
NodeJs
// Node.js 18 or later, in an ES module (.mjs). Keep API keys on the server.
const response = await fetch('https://your-workspace.crewvana.com/api/jobs/100', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Basic ' + Buffer.from('YOUR_API_KEY:').toString('base64'),
},
});
console.log(await response.json());
Python
# Install requests with: python -m pip install requests
import requests
response = requests.request(
'GET',
'https://your-workspace.crewvana.com/api/jobs/100',
headers={'Accept': 'application/json'},
auth=('YOUR_API_KEY', ''),
timeout=30,
)
print(response.json())
Ruby
require 'net/http'
require 'uri'
url = URI('https://your-workspace.crewvana.com/api/jobs/100')
request = Net::HTTP::Get.new(url)
request['Accept'] = 'application/json'
request.basic_auth('YOUR_API_KEY', '')
response = Net::HTTP.start(url.hostname, url.port, use_ssl: true) do |http|
http.request(request)
end
puts response.body
Create a Job with Visits
POST /api/jobs
Authentication: API key and IP allowlist
Creates the complete graph transactionally. Default is draft when unscheduled or Scheduled when a Visit has a schedule. Optional status is draft or Scheduled. Line and Visit inputs are described below. No explicit customer confirmation is sent; normal configured model-trigger Automations still apply.
Input
| Parameter | Format | Required | Description |
|---|---|---|---|
actor_user_id | Integer (JSON body) | Required | Active workspace User whose application permissions authorize the operation. API-key identity is recorded separately. |
Idempotency-Key | HTTP header | Required | 1–100 ASCII letters, digits, dot, underscore, colon or hyphen. Reuse for the same request only. A different method/path/body returns 409; successful replay returns the original status/body and Idempotency-Replayed: true. |
cURL
curl 'https://your-workspace.crewvana.com/api/jobs' \
--request POST \
--user 'YOUR_API_KEY:' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: replace-with-a-unique-request-id' \
--data '{"actor_user_id":2,"contact_id":42,"line_items":[{"type":"item","price_book_item_id":8,"quantity":1,"price":100}],"visits":[{"scheduled_start":"2026-10-05 10:00:00","scheduled_end":"2026-10-05 11:00:00","technician_ids":[2]}]}'
PHP
<?php
$request = curl_init('https://your-workspace.crewvana.com/api/jobs');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_USERPWD => 'YOUR_API_KEY:',
CURLOPT_HTTPHEADER => ['Accept: application/json', 'Content-Type: application/json', 'Idempotency-Key: replace-with-a-unique-request-id'],
CURLOPT_POSTFIELDS => '{"actor_user_id":2,"contact_id":42,"line_items":[{"type":"item","price_book_item_id":8,"quantity":1,"price":100}],"visits":[{"scheduled_start":"2026-10-05 10:00:00","scheduled_end":"2026-10-05 11:00:00","technician_ids":[2]}]}',
CURLOPT_RETURNTRANSFER => true,
]);
echo curl_exec($request);
curl_close($request);
NodeJs
// Run on the server; keep API keys out of browser code.
const response = await fetch('https://your-workspace.crewvana.com/api/jobs', {
method: 'POST',
headers: {
Authorization: 'Bearer YOUR_API_KEY',
Accept: 'application/json',
'Content-Type': 'application/json',
'Idempotency-Key': 'replace-with-a-unique-request-id',
},
body: JSON.stringify({"actor_user_id":2,"contact_id":42,"line_items":[{"type":"item","price_book_item_id":8,"quantity":1,"price":100}],"visits":[{"scheduled_start":"2026-10-05 10:00:00","scheduled_end":"2026-10-05 11:00:00","technician_ids":[2]}]}),
});
console.log(await response.json());
Python
import json
import requests
response = requests.request(
'POST',
'https://your-workspace.crewvana.com/api/jobs',
auth=('YOUR_API_KEY', ''),
headers={'Accept': 'application/json', 'Idempotency-Key': 'replace-with-a-unique-request-id'},
json=json.loads("{\"actor_user_id\":2,\"contact_id\":42,\"line_items\":[{\"type\":\"item\",\"price_book_item_id\":8,\"quantity\":1,\"price\":100}],\"visits\":[{\"scheduled_start\":\"2026-10-05 10:00:00\",\"scheduled_end\":\"2026-10-05 11:00:00\",\"technician_ids\":[2]}]}"),
timeout=30,
)
print(response.json())
Ruby
require 'net/http'
require 'uri'
url = URI('https://your-workspace.crewvana.com/api/jobs')
request = Net::HTTP::Post.new(url)
request.basic_auth('YOUR_API_KEY', '')
request['Accept'] = 'application/json'
request['Content-Type'] = 'application/json'
request['Idempotency-Key'] = 'replace-with-a-unique-request-id'
request.body = "{\"actor_user_id\":2,\"contact_id\":42,\"line_items\":[{\"type\":\"item\",\"price_book_item_id\":8,\"quantity\":1,\"price\":100}],\"visits\":[{\"scheduled_start\":\"2026-10-05 10:00:00\",\"scheduled_end\":\"2026-10-05 11:00:00\",\"technician_ids\":[2]}]}"
response = Net::HTTP.start(url.hostname, url.port, use_ssl: true) { |http| http.request(request) }
puts response.body
Use a fresh request ID for each intended write. Retain the same ID and body when retrying.
Update a Job
PATCH /api/jobs/{job}
Authentication: API key and IP allowlist
Sparse update of contact_id, opportunity_id, contact_address_id, contact_phone_id, contact_email_id, notes, apply_tax, line_items. Related identity and Opportunity IDs must belong to the Contact. Line_items replaces all lines; omit to preserve them. Commercial/customer changes require no protected history. Paid state, totals and execution status are server-owned. Schedule changes use Visit CRUD.
Input
| Parameter | Format | Required | Description |
|---|---|---|---|
actor_user_id | Integer (JSON body) | Required | Active workspace User whose application permissions authorize the operation. API-key identity is recorded separately. |
Idempotency-Key | HTTP header | Required | 1–100 ASCII letters, digits, dot, underscore, colon or hyphen. Reuse for the same request only. A different method/path/body returns 409; successful replay returns the original status/body and Idempotency-Replayed: true. |
expected_updated_at | String (JSON body) | Required for existing Jobs/Visits | Copy the raw updated_at value from GET, formatted YYYY-MM-DD HH:MM:SS. Stale values return 409. It checks the resource row, not every related row. |
cURL
curl 'https://your-workspace.crewvana.com/api/jobs/100' \
--request PATCH \
--user 'YOUR_API_KEY:' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: replace-with-a-unique-request-id' \
--data '{"actor_user_id":2,"expected_updated_at":"2026-10-05 18:00:00","notes":"Updated notes"}'
PHP
<?php
$request = curl_init('https://your-workspace.crewvana.com/api/jobs/100');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'PATCH',
CURLOPT_USERPWD => 'YOUR_API_KEY:',
CURLOPT_HTTPHEADER => ['Accept: application/json', 'Content-Type: application/json', 'Idempotency-Key: replace-with-a-unique-request-id'],
CURLOPT_POSTFIELDS => '{"actor_user_id":2,"expected_updated_at":"2026-10-05 18:00:00","notes":"Updated notes"}',
CURLOPT_RETURNTRANSFER => true,
]);
echo curl_exec($request);
curl_close($request);
NodeJs
// Run on the server; keep API keys out of browser code.
const response = await fetch('https://your-workspace.crewvana.com/api/jobs/100', {
method: 'PATCH',
headers: {
Authorization: 'Bearer YOUR_API_KEY',
Accept: 'application/json',
'Content-Type': 'application/json',
'Idempotency-Key': 'replace-with-a-unique-request-id',
},
body: JSON.stringify({"actor_user_id":2,"expected_updated_at":"2026-10-05 18:00:00","notes":"Updated notes"}),
});
console.log(await response.json());
Python
import json
import requests
response = requests.request(
'PATCH',
'https://your-workspace.crewvana.com/api/jobs/100',
auth=('YOUR_API_KEY', ''),
headers={'Accept': 'application/json', 'Idempotency-Key': 'replace-with-a-unique-request-id'},
json=json.loads("{\"actor_user_id\":2,\"expected_updated_at\":\"2026-10-05 18:00:00\",\"notes\":\"Updated notes\"}"),
timeout=30,
)
print(response.json())
Ruby
require 'net/http'
require 'uri'
url = URI('https://your-workspace.crewvana.com/api/jobs/100')
request = Net::HTTP::Patch.new(url)
request.basic_auth('YOUR_API_KEY', '')
request['Accept'] = 'application/json'
request['Content-Type'] = 'application/json'
request['Idempotency-Key'] = 'replace-with-a-unique-request-id'
request.body = "{\"actor_user_id\":2,\"expected_updated_at\":\"2026-10-05 18:00:00\",\"notes\":\"Updated notes\"}"
response = Net::HTTP.start(url.hostname, url.port, use_ssl: true) { |http| http.request(request) }
puts response.body
Use a fresh request ID for each intended write. Retain the same ID and body when retrying.
Delete a Job draft
DELETE /api/jobs/{job}
Authentication: API key and IP allowlist
Only history-free draft/quoted Jobs can be deleted. Invoices, payments, check-ins, completion, checklist answers, media and compensation protect history. Use guarded cancellation for retained operational records.
Input
| Parameter | Format | Required | Description |
|---|---|---|---|
actor_user_id | Integer (JSON body) | Required | Active workspace User whose application permissions authorize the operation. API-key identity is recorded separately. |
Idempotency-Key | HTTP header | Required | 1–100 ASCII letters, digits, dot, underscore, colon or hyphen. Reuse for the same request only. A different method/path/body returns 409; successful replay returns the original status/body and Idempotency-Replayed: true. |
expected_updated_at | String (JSON body) | Required for existing Jobs/Visits | Copy the raw updated_at value from GET, formatted YYYY-MM-DD HH:MM:SS. Stale values return 409. It checks the resource row, not every related row. |
cURL
curl 'https://your-workspace.crewvana.com/api/jobs/100' \
--request DELETE \
--user 'YOUR_API_KEY:' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: replace-with-a-unique-request-id' \
--data '{"actor_user_id":2,"expected_updated_at":"2026-10-05 18:00:00"}'
PHP
<?php
$request = curl_init('https://your-workspace.crewvana.com/api/jobs/100');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'DELETE',
CURLOPT_USERPWD => 'YOUR_API_KEY:',
CURLOPT_HTTPHEADER => ['Accept: application/json', 'Content-Type: application/json', 'Idempotency-Key: replace-with-a-unique-request-id'],
CURLOPT_POSTFIELDS => '{"actor_user_id":2,"expected_updated_at":"2026-10-05 18:00:00"}',
CURLOPT_RETURNTRANSFER => true,
]);
echo curl_exec($request);
curl_close($request);
NodeJs
// Run on the server; keep API keys out of browser code.
const response = await fetch('https://your-workspace.crewvana.com/api/jobs/100', {
method: 'DELETE',
headers: {
Authorization: 'Bearer YOUR_API_KEY',
Accept: 'application/json',
'Content-Type': 'application/json',
'Idempotency-Key': 'replace-with-a-unique-request-id',
},
body: JSON.stringify({"actor_user_id":2,"expected_updated_at":"2026-10-05 18:00:00"}),
});
console.log(await response.json());
Python
import json
import requests
response = requests.request(
'DELETE',
'https://your-workspace.crewvana.com/api/jobs/100',
auth=('YOUR_API_KEY', ''),
headers={'Accept': 'application/json', 'Idempotency-Key': 'replace-with-a-unique-request-id'},
json=json.loads("{\"actor_user_id\":2,\"expected_updated_at\":\"2026-10-05 18:00:00\"}"),
timeout=30,
)
print(response.json())
Ruby
require 'net/http'
require 'uri'
url = URI('https://your-workspace.crewvana.com/api/jobs/100')
request = Net::HTTP::Delete.new(url)
request.basic_auth('YOUR_API_KEY', '')
request['Accept'] = 'application/json'
request['Content-Type'] = 'application/json'
request['Idempotency-Key'] = 'replace-with-a-unique-request-id'
request.body = "{\"actor_user_id\":2,\"expected_updated_at\":\"2026-10-05 18:00:00\"}"
response = Net::HTTP.start(url.hostname, url.port, use_ssl: true) { |http| http.request(request) }
puts response.body
Use a fresh request ID for each intended write. Retain the same ID and body when retrying.
Cancel a Job
POST /api/jobs/{job}/cancel
Authentication: API key and IP allowlist
Uses canonical Job cancellation, Invoice lifecycle, Subscription exceptions and protected-future rules. Returns affected/protected Job IDs. No customer notification is sent.
Input
| Parameter | Format | Required | Description |
|---|---|---|---|
actor_user_id | Integer (JSON body) | Required | Active workspace User whose application permissions authorize the operation. API-key identity is recorded separately. |
Idempotency-Key | HTTP header | Required | 1–100 ASCII letters, digits, dot, underscore, colon or hyphen. Reuse for the same request only. A different method/path/body returns 409; successful replay returns the original status/body and Idempotency-Replayed: true. |
expected_updated_at | String (JSON body) | Required for existing Jobs/Visits | Copy the raw updated_at value from GET, formatted YYYY-MM-DD HH:MM:SS. Stale values return 409. It checks the resource row, not every related row. |
reason | String, max 120 | Required | Nonblank reason. |
scope | String | Required | one_occurrence or this_and_future |
confirm_future | Boolean | Required for this_and_future | Must be true. |
billing_decision | String | Conditional | retain_invoice when completed work or collected money requires retaining charges; requires payments.update. |
cURL
curl 'https://your-workspace.crewvana.com/api/jobs/100/cancel' \
--request POST \
--user 'YOUR_API_KEY:' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: replace-with-a-unique-request-id' \
--data '{"actor_user_id":2,"expected_updated_at":"2026-10-05 18:00:00","reason":"Duplicate booking","scope":"one_occurrence"}'
PHP
<?php
$request = curl_init('https://your-workspace.crewvana.com/api/jobs/100/cancel');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_USERPWD => 'YOUR_API_KEY:',
CURLOPT_HTTPHEADER => ['Accept: application/json', 'Content-Type: application/json', 'Idempotency-Key: replace-with-a-unique-request-id'],
CURLOPT_POSTFIELDS => '{"actor_user_id":2,"expected_updated_at":"2026-10-05 18:00:00","reason":"Duplicate booking","scope":"one_occurrence"}',
CURLOPT_RETURNTRANSFER => true,
]);
echo curl_exec($request);
curl_close($request);
NodeJs
// Run on the server; keep API keys out of browser code.
const response = await fetch('https://your-workspace.crewvana.com/api/jobs/100/cancel', {
method: 'POST',
headers: {
Authorization: 'Bearer YOUR_API_KEY',
Accept: 'application/json',
'Content-Type': 'application/json',
'Idempotency-Key': 'replace-with-a-unique-request-id',
},
body: JSON.stringify({"actor_user_id":2,"expected_updated_at":"2026-10-05 18:00:00","reason":"Duplicate booking","scope":"one_occurrence"}),
});
console.log(await response.json());
Python
import json
import requests
response = requests.request(
'POST',
'https://your-workspace.crewvana.com/api/jobs/100/cancel',
auth=('YOUR_API_KEY', ''),
headers={'Accept': 'application/json', 'Idempotency-Key': 'replace-with-a-unique-request-id'},
json=json.loads("{\"actor_user_id\":2,\"expected_updated_at\":\"2026-10-05 18:00:00\",\"reason\":\"Duplicate booking\",\"scope\":\"one_occurrence\"}"),
timeout=30,
)
print(response.json())
Ruby
require 'net/http'
require 'uri'
url = URI('https://your-workspace.crewvana.com/api/jobs/100/cancel')
request = Net::HTTP::Post.new(url)
request.basic_auth('YOUR_API_KEY', '')
request['Accept'] = 'application/json'
request['Content-Type'] = 'application/json'
request['Idempotency-Key'] = 'replace-with-a-unique-request-id'
request.body = "{\"actor_user_id\":2,\"expected_updated_at\":\"2026-10-05 18:00:00\",\"reason\":\"Duplicate booking\",\"scope\":\"one_occurrence\"}"
response = Net::HTTP.start(url.hostname, url.port, use_ssl: true) { |http| http.request(request) }
puts response.body
Use a fresh request ID for each intended write. Retain the same ID and body when retrying.
List Visits
GET /api/visits
Authentication: API key and IP allowlist
Optional filters: service_job_id, contact_id, technician_id, status, from, to. Timestamp bounds use YYYY-MM-DD HH:MM:SS and inclusive comparisons against stored schedule values. Results are ordered by scheduled_start then ID.
Input
| Parameter | Format | Required | Description |
|---|---|---|---|
limit | Integer | Optional | 1–100, default 25. |
offset | Integer | Optional | 0–10000, default 0. |
cURL
curl 'https://your-workspace.crewvana.com/api/visits?limit=25' \
--header 'Accept: application/json' \
--user 'YOUR_API_KEY:'
PHP
<?php
$request = curl_init('https://your-workspace.crewvana.com/api/visits?limit=25');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => ['Accept: application/json'],
CURLOPT_USERPWD => 'YOUR_API_KEY:',
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($request);
curl_close($request);
echo $response;
NodeJs
// Node.js 18 or later, in an ES module (.mjs). Keep API keys on the server.
const response = await fetch('https://your-workspace.crewvana.com/api/visits?limit=25', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Basic ' + Buffer.from('YOUR_API_KEY:').toString('base64'),
},
});
console.log(await response.json());
Python
# Install requests with: python -m pip install requests
import requests
response = requests.request(
'GET',
'https://your-workspace.crewvana.com/api/visits?limit=25',
headers={'Accept': 'application/json'},
auth=('YOUR_API_KEY', ''),
timeout=30,
)
print(response.json())
Ruby
require 'net/http'
require 'uri'
url = URI('https://your-workspace.crewvana.com/api/visits?limit=25')
request = Net::HTTP::Get.new(url)
request['Accept'] = 'application/json'
request.basic_auth('YOUR_API_KEY', '')
response = Net::HTTP.start(url.hostname, url.port, use_ssl: true) do |http|
http.request(request)
end
puts response.body
List a Job’s Visits
GET /api/jobs/{job}/visits
Authentication: API key and IP allowlist
Same fields, filters and bounds as /api/visits, constrained to the path Job. Unknown Job returns 404.
Input
| Parameter | Format | Required | Description |
|---|---|---|---|
limit | Integer | Optional | 1–100, default 25. |
offset | Integer | Optional | 0–10000, default 0. |
cURL
curl 'https://your-workspace.crewvana.com/api/jobs/100/visits?limit=25' \
--header 'Accept: application/json' \
--user 'YOUR_API_KEY:'
PHP
<?php
$request = curl_init('https://your-workspace.crewvana.com/api/jobs/100/visits?limit=25');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => ['Accept: application/json'],
CURLOPT_USERPWD => 'YOUR_API_KEY:',
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($request);
curl_close($request);
echo $response;
NodeJs
// Node.js 18 or later, in an ES module (.mjs). Keep API keys on the server.
const response = await fetch('https://your-workspace.crewvana.com/api/jobs/100/visits?limit=25', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Basic ' + Buffer.from('YOUR_API_KEY:').toString('base64'),
},
});
console.log(await response.json());
Python
# Install requests with: python -m pip install requests
import requests
response = requests.request(
'GET',
'https://your-workspace.crewvana.com/api/jobs/100/visits?limit=25',
headers={'Accept': 'application/json'},
auth=('YOUR_API_KEY', ''),
timeout=30,
)
print(response.json())
Ruby
require 'net/http'
require 'uri'
url = URI('https://your-workspace.crewvana.com/api/jobs/100/visits?limit=25')
request = Net::HTTP::Get.new(url)
request['Accept'] = 'application/json'
request.basic_auth('YOUR_API_KEY', '')
response = Net::HTTP.start(url.hostname, url.port, use_ssl: true) do |http|
http.request(request)
end
puts response.body
Read a Visit’s execution records
GET /api/visits/{visit}
Authentication: API key and IP allowlist
Returns Visit identity, status, schedule and up to 100 technician check-ins; paginated line completion rows, checklist review summaries and answers with actor attribution. No provider data or customer payment links.
Input
| Parameter | Format | Required | Description |
|---|---|---|---|
line_limit / checklist_limit / response_limit | Integer | Optional | 1–100, default 100. |
line_offset / checklist_offset / response_offset | Integer | Optional | 0–10000, default 0; continue each nested page. |
cURL
curl 'https://your-workspace.crewvana.com/api/visits/101' \
--header 'Accept: application/json' \
--user 'YOUR_API_KEY:'
PHP
<?php
$request = curl_init('https://your-workspace.crewvana.com/api/visits/101');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => ['Accept: application/json'],
CURLOPT_USERPWD => 'YOUR_API_KEY:',
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($request);
curl_close($request);
echo $response;
NodeJs
// Node.js 18 or later, in an ES module (.mjs). Keep API keys on the server.
const response = await fetch('https://your-workspace.crewvana.com/api/visits/101', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Basic ' + Buffer.from('YOUR_API_KEY:').toString('base64'),
},
});
console.log(await response.json());
Python
# Install requests with: python -m pip install requests
import requests
response = requests.request(
'GET',
'https://your-workspace.crewvana.com/api/visits/101',
headers={'Accept': 'application/json'},
auth=('YOUR_API_KEY', ''),
timeout=30,
)
print(response.json())
Ruby
require 'net/http'
require 'uri'
url = URI('https://your-workspace.crewvana.com/api/visits/101')
request = Net::HTTP::Get.new(url)
request['Accept'] = 'application/json'
request.basic_auth('YOUR_API_KEY', '')
response = Net::HTTP.start(url.hostname, url.port, use_ssl: true) do |http|
http.request(request)
end
puts response.body
Add a Visit to a Job
POST /api/jobs/{job}/visits
Authentication: API key and IP allowlist
Creates a non-default Visit with the next sequence. Requires Job update-all permission. Completed/cancelled Jobs cannot gain Visits through CRUD.
Input
| Parameter | Format | Required | Description |
|---|---|---|---|
actor_user_id | Integer (JSON body) | Required | Active workspace User whose application permissions authorize the operation. API-key identity is recorded separately. |
Idempotency-Key | HTTP header | Required | 1–100 ASCII letters, digits, dot, underscore, colon or hyphen. Reuse for the same request only. A different method/path/body returns 409; successful replay returns the original status/body and Idempotency-Replayed: true. |
cURL
curl 'https://your-workspace.crewvana.com/api/jobs/100/visits' \
--request POST \
--user 'YOUR_API_KEY:' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: replace-with-a-unique-request-id' \
--data '{"actor_user_id":2,"scheduled_start":"2026-10-06 10:00:00","scheduled_end":"2026-10-06 11:00:00","technician_ids":[2]}'
PHP
<?php
$request = curl_init('https://your-workspace.crewvana.com/api/jobs/100/visits');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_USERPWD => 'YOUR_API_KEY:',
CURLOPT_HTTPHEADER => ['Accept: application/json', 'Content-Type: application/json', 'Idempotency-Key: replace-with-a-unique-request-id'],
CURLOPT_POSTFIELDS => '{"actor_user_id":2,"scheduled_start":"2026-10-06 10:00:00","scheduled_end":"2026-10-06 11:00:00","technician_ids":[2]}',
CURLOPT_RETURNTRANSFER => true,
]);
echo curl_exec($request);
curl_close($request);
NodeJs
// Run on the server; keep API keys out of browser code.
const response = await fetch('https://your-workspace.crewvana.com/api/jobs/100/visits', {
method: 'POST',
headers: {
Authorization: 'Bearer YOUR_API_KEY',
Accept: 'application/json',
'Content-Type': 'application/json',
'Idempotency-Key': 'replace-with-a-unique-request-id',
},
body: JSON.stringify({"actor_user_id":2,"scheduled_start":"2026-10-06 10:00:00","scheduled_end":"2026-10-06 11:00:00","technician_ids":[2]}),
});
console.log(await response.json());
Python
import json
import requests
response = requests.request(
'POST',
'https://your-workspace.crewvana.com/api/jobs/100/visits',
auth=('YOUR_API_KEY', ''),
headers={'Accept': 'application/json', 'Idempotency-Key': 'replace-with-a-unique-request-id'},
json=json.loads("{\"actor_user_id\":2,\"scheduled_start\":\"2026-10-06 10:00:00\",\"scheduled_end\":\"2026-10-06 11:00:00\",\"technician_ids\":[2]}"),
timeout=30,
)
print(response.json())
Ruby
require 'net/http'
require 'uri'
url = URI('https://your-workspace.crewvana.com/api/jobs/100/visits')
request = Net::HTTP::Post.new(url)
request.basic_auth('YOUR_API_KEY', '')
request['Accept'] = 'application/json'
request['Content-Type'] = 'application/json'
request['Idempotency-Key'] = 'replace-with-a-unique-request-id'
request.body = "{\"actor_user_id\":2,\"scheduled_start\":\"2026-10-06 10:00:00\",\"scheduled_end\":\"2026-10-06 11:00:00\",\"technician_ids\":[2]}"
response = Net::HTTP.start(url.hostname, url.port, use_ssl: true) { |http| http.request(request) }
puts response.body
Use a fresh request ID for each intended write. Retain the same ID and body when retrying.
Update a Visit schedule or assignment
PATCH /api/visits/{visit}
Authentication: API key and IP allowlist
Sparse update of scheduled_start/end, technician_ids, allow_block_overlap. A null start unschedules the Visit. No implicit Job creation. Completed/cancelled Visits and Jobs require their lifecycle workflow. Other-technician assignment requires update-all. Sequence, default identity, execution status and check-in timestamps cannot be overwritten.
Input
| Parameter | Format | Required | Description |
|---|---|---|---|
actor_user_id | Integer (JSON body) | Required | Active workspace User whose application permissions authorize the operation. API-key identity is recorded separately. |
Idempotency-Key | HTTP header | Required | 1–100 ASCII letters, digits, dot, underscore, colon or hyphen. Reuse for the same request only. A different method/path/body returns 409; successful replay returns the original status/body and Idempotency-Replayed: true. |
expected_updated_at | String (JSON body) | Required for existing Jobs/Visits | Copy the raw updated_at value from GET, formatted YYYY-MM-DD HH:MM:SS. Stale values return 409. It checks the resource row, not every related row. |
cURL
curl 'https://your-workspace.crewvana.com/api/visits/101' \
--request PATCH \
--user 'YOUR_API_KEY:' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: replace-with-a-unique-request-id' \
--data '{"actor_user_id":2,"expected_updated_at":"2026-10-05 18:00:00","scheduled_start":"2026-10-06 13:00:00","scheduled_end":"2026-10-06 14:00:00"}'
PHP
<?php
$request = curl_init('https://your-workspace.crewvana.com/api/visits/101');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'PATCH',
CURLOPT_USERPWD => 'YOUR_API_KEY:',
CURLOPT_HTTPHEADER => ['Accept: application/json', 'Content-Type: application/json', 'Idempotency-Key: replace-with-a-unique-request-id'],
CURLOPT_POSTFIELDS => '{"actor_user_id":2,"expected_updated_at":"2026-10-05 18:00:00","scheduled_start":"2026-10-06 13:00:00","scheduled_end":"2026-10-06 14:00:00"}',
CURLOPT_RETURNTRANSFER => true,
]);
echo curl_exec($request);
curl_close($request);
NodeJs
// Run on the server; keep API keys out of browser code.
const response = await fetch('https://your-workspace.crewvana.com/api/visits/101', {
method: 'PATCH',
headers: {
Authorization: 'Bearer YOUR_API_KEY',
Accept: 'application/json',
'Content-Type': 'application/json',
'Idempotency-Key': 'replace-with-a-unique-request-id',
},
body: JSON.stringify({"actor_user_id":2,"expected_updated_at":"2026-10-05 18:00:00","scheduled_start":"2026-10-06 13:00:00","scheduled_end":"2026-10-06 14:00:00"}),
});
console.log(await response.json());
Python
import json
import requests
response = requests.request(
'PATCH',
'https://your-workspace.crewvana.com/api/visits/101',
auth=('YOUR_API_KEY', ''),
headers={'Accept': 'application/json', 'Idempotency-Key': 'replace-with-a-unique-request-id'},
json=json.loads("{\"actor_user_id\":2,\"expected_updated_at\":\"2026-10-05 18:00:00\",\"scheduled_start\":\"2026-10-06 13:00:00\",\"scheduled_end\":\"2026-10-06 14:00:00\"}"),
timeout=30,
)
print(response.json())
Ruby
require 'net/http'
require 'uri'
url = URI('https://your-workspace.crewvana.com/api/visits/101')
request = Net::HTTP::Patch.new(url)
request.basic_auth('YOUR_API_KEY', '')
request['Accept'] = 'application/json'
request['Content-Type'] = 'application/json'
request['Idempotency-Key'] = 'replace-with-a-unique-request-id'
request.body = "{\"actor_user_id\":2,\"expected_updated_at\":\"2026-10-05 18:00:00\",\"scheduled_start\":\"2026-10-06 13:00:00\",\"scheduled_end\":\"2026-10-06 14:00:00\"}"
response = Net::HTTP.start(url.hostname, url.port, use_ssl: true) { |http| http.request(request) }
puts response.body
Use a fresh request ID for each intended write. Retain the same ID and body when retrying.
Delete a Visit
DELETE /api/visits/{visit}
Authentication: API key and IP allowlist
Default/only Visits and Visits with execution, media or generated occurrence history cannot be deleted. Remaining sequence and checklist assignment are reconciled canonically.
Input
| Parameter | Format | Required | Description |
|---|---|---|---|
actor_user_id | Integer (JSON body) | Required | Active workspace User whose application permissions authorize the operation. API-key identity is recorded separately. |
Idempotency-Key | HTTP header | Required | 1–100 ASCII letters, digits, dot, underscore, colon or hyphen. Reuse for the same request only. A different method/path/body returns 409; successful replay returns the original status/body and Idempotency-Replayed: true. |
expected_updated_at | String (JSON body) | Required for existing Jobs/Visits | Copy the raw updated_at value from GET, formatted YYYY-MM-DD HH:MM:SS. Stale values return 409. It checks the resource row, not every related row. |
cURL
curl 'https://your-workspace.crewvana.com/api/visits/101' \
--request DELETE \
--user 'YOUR_API_KEY:' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: replace-with-a-unique-request-id' \
--data '{"actor_user_id":2,"expected_updated_at":"2026-10-05 18:00:00"}'
PHP
<?php
$request = curl_init('https://your-workspace.crewvana.com/api/visits/101');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'DELETE',
CURLOPT_USERPWD => 'YOUR_API_KEY:',
CURLOPT_HTTPHEADER => ['Accept: application/json', 'Content-Type: application/json', 'Idempotency-Key: replace-with-a-unique-request-id'],
CURLOPT_POSTFIELDS => '{"actor_user_id":2,"expected_updated_at":"2026-10-05 18:00:00"}',
CURLOPT_RETURNTRANSFER => true,
]);
echo curl_exec($request);
curl_close($request);
NodeJs
// Run on the server; keep API keys out of browser code.
const response = await fetch('https://your-workspace.crewvana.com/api/visits/101', {
method: 'DELETE',
headers: {
Authorization: 'Bearer YOUR_API_KEY',
Accept: 'application/json',
'Content-Type': 'application/json',
'Idempotency-Key': 'replace-with-a-unique-request-id',
},
body: JSON.stringify({"actor_user_id":2,"expected_updated_at":"2026-10-05 18:00:00"}),
});
console.log(await response.json());
Python
import json
import requests
response = requests.request(
'DELETE',
'https://your-workspace.crewvana.com/api/visits/101',
auth=('YOUR_API_KEY', ''),
headers={'Accept': 'application/json', 'Idempotency-Key': 'replace-with-a-unique-request-id'},
json=json.loads("{\"actor_user_id\":2,\"expected_updated_at\":\"2026-10-05 18:00:00\"}"),
timeout=30,
)
print(response.json())
Ruby
require 'net/http'
require 'uri'
url = URI('https://your-workspace.crewvana.com/api/visits/101')
request = Net::HTTP::Delete.new(url)
request.basic_auth('YOUR_API_KEY', '')
request['Accept'] = 'application/json'
request['Content-Type'] = 'application/json'
request['Idempotency-Key'] = 'replace-with-a-unique-request-id'
request.body = "{\"actor_user_id\":2,\"expected_updated_at\":\"2026-10-05 18:00:00\"}"
response = Net::HTTP.start(url.hostname, url.port, use_ssl: true) { |http| http.request(request) }
puts response.body
Use a fresh request ID for each intended write. Retain the same ID and body when retrying.
Cancel one Visit
POST /api/visits/{visit}/cancel
Authentication: API key and IP allowlist
Cancels a non-completed Visit. Use Job cancellation for the last required Visit so billing and Subscription exceptions are reviewed. Does not send a message.
Input
| Parameter | Format | Required | Description |
|---|---|---|---|
actor_user_id | Integer (JSON body) | Required | Active workspace User whose application permissions authorize the operation. API-key identity is recorded separately. |
Idempotency-Key | HTTP header | Required | 1–100 ASCII letters, digits, dot, underscore, colon or hyphen. Reuse for the same request only. A different method/path/body returns 409; successful replay returns the original status/body and Idempotency-Replayed: true. |
expected_updated_at | String (JSON body) | Required for existing Jobs/Visits | Copy the raw updated_at value from GET, formatted YYYY-MM-DD HH:MM:SS. Stale values return 409. It checks the resource row, not every related row. |
cURL
curl 'https://your-workspace.crewvana.com/api/visits/101/cancel' \
--request POST \
--user 'YOUR_API_KEY:' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: replace-with-a-unique-request-id' \
--data '{"actor_user_id":2,"expected_updated_at":"2026-10-05 18:00:00"}'
PHP
<?php
$request = curl_init('https://your-workspace.crewvana.com/api/visits/101/cancel');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_USERPWD => 'YOUR_API_KEY:',
CURLOPT_HTTPHEADER => ['Accept: application/json', 'Content-Type: application/json', 'Idempotency-Key: replace-with-a-unique-request-id'],
CURLOPT_POSTFIELDS => '{"actor_user_id":2,"expected_updated_at":"2026-10-05 18:00:00"}',
CURLOPT_RETURNTRANSFER => true,
]);
echo curl_exec($request);
curl_close($request);
NodeJs
// Run on the server; keep API keys out of browser code.
const response = await fetch('https://your-workspace.crewvana.com/api/visits/101/cancel', {
method: 'POST',
headers: {
Authorization: 'Bearer YOUR_API_KEY',
Accept: 'application/json',
'Content-Type': 'application/json',
'Idempotency-Key': 'replace-with-a-unique-request-id',
},
body: JSON.stringify({"actor_user_id":2,"expected_updated_at":"2026-10-05 18:00:00"}),
});
console.log(await response.json());
Python
import json
import requests
response = requests.request(
'POST',
'https://your-workspace.crewvana.com/api/visits/101/cancel',
auth=('YOUR_API_KEY', ''),
headers={'Accept': 'application/json', 'Idempotency-Key': 'replace-with-a-unique-request-id'},
json=json.loads("{\"actor_user_id\":2,\"expected_updated_at\":\"2026-10-05 18:00:00\"}"),
timeout=30,
)
print(response.json())
Ruby
require 'net/http'
require 'uri'
url = URI('https://your-workspace.crewvana.com/api/visits/101/cancel')
request = Net::HTTP::Post.new(url)
request.basic_auth('YOUR_API_KEY', '')
request['Accept'] = 'application/json'
request['Content-Type'] = 'application/json'
request['Idempotency-Key'] = 'replace-with-a-unique-request-id'
request.body = "{\"actor_user_id\":2,\"expected_updated_at\":\"2026-10-05 18:00:00\"}"
response = Net::HTTP.start(url.hostname, url.port, use_ssl: true) { |http| http.request(request) }
puts response.body
Use a fresh request ID for each intended write. Retain the same ID and body when retrying.