Download OpenAPI specification:
Read and write a ProLine company's projects, contacts, events, activities and files on behalf of an integration partner.
The Partner API lets an approved integration partner read and write data in a
ProLine company that has shared its company key with the partner. Every
request is a POST with a JSON body to a fixed path under
https://api.proline.app/v1/, authenticated with two keys (see
Authentication). Responses are JSON.
Endpoints are grouped by the record they work on. Lists are paged; finds
return a single record (or an empty array when nothing matches); edits
create or update; imports create records in bulk-friendly shapes and are
idempotent on an external_id you supply.
Two keys travel as request headers on every call:
| Header | What it identifies | Where it comes from |
|---|---|---|
PARTNER_KEY |
Your integration. One key per partner, used against every company you serve. | Issued by ProLine when your application is approved. |
COMPANY_KEY |
The ProLine company the request acts on. | The company's admin copies it from Settings → Integrations → ProLine API and gives it to you. |
A request with a missing or unknown key is refused with 401. Rate limits
(below) are per company key, so one slow integration never affects another
company's.
Keep both keys server-side. The company key in particular grants access to that company's customer data; treat it like a password.
Limits are enforced per company key and per path, as a fixed interval
between calls. A call inside the interval is refused with 429 and a body
of { "error": "Rate limit exceeded for <path>. Try again in <n> seconds." }.
| Paths | Interval |
|---|---|
/v1/find/contact, /v1/edit/contact, /v1/import/contact |
1 call per 3 seconds |
/v1/import/file |
1 call per second |
/v1/import/activity |
1 call per 2 seconds |
/v1/import/tags |
1 call per 10 seconds |
| Every other path | 1 call per 5 seconds |
Lists return up to 100 records per page; a larger limit is clamped to 100.
Two conventions exist, for historical reasons, and each endpoint says which it follows:
Direct. The newer endpoints (every /v1/list/* path, /v1/find/activity)
answer with the record data as the whole body and a meaningful HTTP status:
200 on success, 400 for an invalid request, 401/403 for a key
problem, 429 when rate limited.
Wrapped. The original endpoints answer 200 whatever happened and wrap
the result in a fixed envelope:
{
"isBase64Encoded": false,
"statusCode": 200,
"statusDescription": "200 OK",
"headers": { "Content-Type": "application/json" },
"body": { ... }
}
The result, including any error the backend reported, is in body. Check
body for an error key before treating the call as successful. Only
errors raised before the request reaches the backend (an unknown key, a
rate limit, an invalid JSON body, an unknown body key) carry a real HTTP
status and a flat { "error": "..." } body.
| Status | Meaning | Body |
|---|---|---|
400 |
Invalid JSON, a body key the endpoint does not accept (Invalid key found: "<key>"), or a missing required key. |
{ "error": "..." } |
401 |
Unknown or inactive partner key, or unknown company key. | { "error": "..." } |
403 |
The partner key is not permitted to call this endpoint. | { "error": "Permission Denied" } |
404 |
Unknown path. | { "error": "..." } |
429 |
Rate limited (see Rate limits). | { "error": "..." } |
Backend validation errors on the direct endpoints use
{ "success": false, "error": { "code": "BAD_REQUEST", "message": "..." } }.
Ids in responses are ProLine ids (opaque strings). Where a company was migrated from the original ProLine platform, requests may also use that platform's legacy ids for the same records, and activity records of migrated data still report their legacy ids. Treat every id as an opaque string.
ProLine can call your endpoint when records change. Outbound webhooks are configured by the company in ProLine, not through this API; see ProLine Webhooks.
Pages through every project in the company, newest first, with its stage,
contacts, budget figures and status dates. Filter by creation date with
created_after / created_before (both inclusive). Pages are limited to
100 rows and to an offset of 10,000 rows; beyond that, or when a page would
read more than the per-call budget, the call answers 400 with a
{ "status": "error", "message": "..." } body and you should narrow the
date window. total is exact up to 4,000 matching rows and a lower bound
past that; has_more is authoritative. project_notes is returned as
plain text.
Response shape: direct (see Overview → Response shapes).
| page | integer >= 1 Default: 1 1-based page number. |
| limit | integer [ 1 .. 100 ] Default: 100 Rows per page. A value above 100 is clamped to 100. |
| created_after | string or null <date-time> Only projects created at or after this instant. A full ISO 8601 datetime with timezone (for example |
| created_before | string or null <date-time> Only projects created at or before this instant. Same format as |
{- "page": 1,
- "limit": 50,
- "created_after": "2026-09-01T00:00:00Z"
}{- "page": 1,
- "limit": 50,
- "total": 812,
- "has_more": true,
- "results": [
- {
- "project_id": "k57abc1n0cq8vb3d5t6hm4ws2e7a9zq1",
- "project_name": "Smith Re-roof",
- "assigned_to_id": "js9f2x1n0cq8vb3d5t6hm4ws2e7a9zq2",
- "assigned_to_name": "Dana Reyes",
- "assigned_to_email": "dana@example.com",
- "project_number": "P-1042",
- "project_address1": "12 Elm St",
- "project_address2": "",
- "project_city": "Reno",
- "project_state": "NV",
- "project_zip": "89501",
- "project_category": "Residential",
- "project_type": "Replacement",
- "project_services": [
- "Roofing"
], - "project_tags": [ ],
- "project_area": "",
- "project_location": "North",
- "project_status": "Open",
- "project_stage": "Estimate",
- "project_lead_status": "",
- "project_inspection_status": "",
- "project_status_date_lead": "2026-09-02T15:04:05.000Z",
- "project_status_date_inspect": null,
- "project_status_date_open": "2026-09-03T10:00:00.000Z",
- "project_status_date_disqualified": null,
- "project_status_date_lost": null,
- "project_status_date_won": null,
- "project_status_date_funded": null,
- "project_status_date_unfunded": null,
- "project_status_date_complete": null,
- "project_status_date_closed": null,
- "project_last_stage_change": "2026-09-03T10:00:00.000Z",
- "project_main_contact_id": "c1xf2x1n0cq8vb3d5t6hm4ws2e7a9zq3",
- "project_other_contact_1_id": null,
- "project_other_contact_2_id": null,
- "project_referred_by_id": "",
- "project_organization_name": "",
- "project_lead_source_name": "Website",
- "project_budget_accounts_receivable": 0,
- "project_budget_average_quoted_value": 18500,
- "project_budget_ach_returns": 0,
- "project_budget_chargebacks": 0,
- "project_budget_gross_margin": 0.32,
- "project_budget_gross_profit_actual": 0,
- "project_budget_gross_profit_planned": 5920,
- "project_budget_gross_revenue_actual": 0,
- "project_budget_gross_revenue_planned": null,
- "project_budget_merchant_fees": 0,
- "project_budget_net_revenue": 0,
- "project_budget_pre_comms_gross": 0,
- "project_budget_project_costs_actual": null,
- "project_budget_project_costs_planned": 12580,
- "project_budget_refunds": 0,
- "project_budget_tax": null,
- "project_notes": "Customer prefers mornings",
- "created_at": "2026-09-02T15:04:05.000Z",
- "updated_at": "2026-09-03T10:00:00.000Z"
}
]
}Looks up one project and returns the first match. The keys are tried in
this order: project_id, then project_external_id, then contact_id
(the first project whose main contact is that contact), then the address
fields together. project_stage and project_status do not find a
project on their own; they filter the one that was found, so a project
in another stage or status answers [].
body is an array with one project, or an empty array when nothing
matched (there is no not-found error). An id that does not resolve, in
the lookup or in a filter, also answers [].
Response shape: wrapped (see Overview → Response shapes).
| project_id | string (ProLineId) A ProLine record id. Opaque; in requests, a legacy id for the same record is also accepted. |
| project_external_id | string The project's |
| contact_id | string Main contact id; a legacy id is also accepted. |
| project_address1 | string Street line, used with |
| project_address2 | string Accepted and ignored. |
| project_city | string |
| project_state | string |
| project_zip | string |
| project_stage | string Stage id (see |
| project_status | string Enum: "lead" "inspection" "open" "disqualified" "lost" "won" "unfunded" "funded" "complete" "closed" Filter: the found project's stage must have this status. The
underlying value, not the label the response returns in
|
{- "project_external_id": "CRM-10042"
}{- "isBase64Encoded": false,
- "statusCode": 200,
- "statusDescription": "200 OK",
- "headers": {
- "Content-Type": "application/json"
}, - "body": [
- {
- "project_name": "Ortiz re-roof",
- "project_number": "1042",
- "project_id": "p12d8f6h4tq0nb2v5k9xm3wr7se1c4ya",
- "stage": "Lead",
- "stage_id": "k97f2x1n0cq8vb3d5t6hm4ws2e7a9zq1",
- "address1": "12 Elm St",
- "address2": "",
- "city": "Austin",
- "state": "TX",
- "zip": "78701",
- "custom_field_1": "",
- "custom_field_2": "",
- "custom_field_3": "",
- "custom_field_4": "",
- "custom_field_5": "",
- "custom_field_6": "",
- "custom_field_7": "",
- "assigned_to_id": "j97b4d2f8kq1nc5v7h3xm6tw9se0a2rz",
- "assigned_to_name": "Sam Rep",
- "assigned_to_email": "sam@roofer.example",
- "assigned_to_proline": "j97b4d2f8kq1nc5v7h3xm6tw9se0a2rz",
- "notes": "",
- "contact_id": "k57a2c9d1xq4nb8v6m3wt5hs0e7r1pz2",
- "contact_display": "Dana Ortiz",
- "contact_fname": "Dana",
- "contact_lname": "Ortiz",
- "contact_phone": "+15125550142",
- "contact_email": "dana@example.com",
- "other_contact_1_id": "",
- "other_contact_1_display": "",
- "other_contact_1_fname": "",
- "other_contact_1_lname": "",
- "other_contact_1_phone": "",
- "other_contact_1_email": "",
- "other_contact_2_id": "",
- "other_contact_2_display": "",
- "other_contact_2_fname": "",
- "other_contact_2_lname": "",
- "other_contact_2_phone": "",
- "other_contact_2_email": "",
- "status": "Lead",
- "category": "Residential",
- "type": "Re-roof",
- "services": [
- "Roofing"
], - "tags": [
- "insurance"
], - "quoted_value": 18500,
- "approved_value": 0,
- "accounts_receivable": 0,
- "gross_revenue": 0,
- "net_revenue": 0,
- "gross_profit": 0,
- "gross_margin": 0,
- "merchant_fees": 0,
- "refunds": 0,
- "chargebacks": 0,
- "contact_lead_source": "Referral"
}
]
}Updates an existing project and its main contact, or creates both, in one call, and optionally schedules an event on the project.
Create or update is decided in this order: project_id; otherwise the
one project whose external_id matches (two or more matches are
rejected, send project_id); otherwise, when edit_contact_latest is
true, the contact's newest open project; otherwise a new project is
created. external_id_new renames the external id and is refused when
another project holds it.
The contact is resolved by contact_id, then contact_phone, then
contact_email, then the existing project's main contact, then an
unsaved draft with that phone; otherwise a contact is created, which
requires contact_fname or contact_name.
Omitted or blank fields are left unchanged on both the project and the
contact, so a value cannot be cleared through this endpoint. The one
exception is project_tags: when present it replaces the whole tag
list, and "" or [] clears it. A new project with no project_stage
goes into the first stage of the first project pipeline. Ids that do not
resolve (project_assign, project_category, project_location, ...)
are dropped and the call still succeeds.
Side effects: a contact may be created, default folders are created on a
new project, field changes are logged as activities and project webhooks
fire. An event scheduling failure never fails the call; event_scheduled
is then "null". An event_external_id held by an event on another
project is skipped.
Rejections arrive inside body.response with error: true (the outer
status is still "success"). When the company's automations are
paused, body is { "ok": true } and nothing is written.
Response shape: wrapped (see Overview → Response shapes).
| project_id | string (ProLineId) A ProLine record id. Opaque; in requests, a legacy id for the same record is also accepted. |
| external_id | string Your own id for the project; lookup key when |
| external_id_new | string New |
| edit_contact_latest | string
|
| project_name | string |
| project_address1 | string |
| project_address2 | string |
| project_city | string |
| project_state | string |
| project_zip | string |
| project_notes | string |
| project_location | string Location id. |
| project_assign | string Team member id for the sales assignment. |
| project_inside_sales | string Team member id. |
| project_production | string Team member id. |
| project_accounting | string Team member id. |
| project_category | string Project category id. |
| project_type | string Project type id. |
| project_stage | string Stage id (see |
| project_tags | Array of strings or string Tag ids, as an array or a comma-separated string. Replaces
the whole tag list; |
| project_custom_field_1 | string Written to the first legacy project custom field slot. Skipped when the company has no such field. |
| project_custom_field_2 | string |
| project_custom_field_3 | string |
| project_custom_field_4 | string |
| project_custom_field_5 | string |
| project_custom_field_6 | string |
| project_custom_field_7 | string |
| project_services | string Accepted and ignored. |
| project_area | string Accepted and ignored. |
| contact_campaign | string Campaign id for attribution. |
| contact_id | string Main contact id; a legacy id is also accepted. |
| contact_fname | string First name. One of this or |
| contact_name | string Full name, used when creating the contact without |
| contact_phone | string Any format; also a contact lookup key. |
| contact_email | string Also a contact lookup key. |
| contact_type | string Contact type id. |
| contact_lead_source | string Lead source id. |
| contact_tags | Array of strings or string Contact tag ids, as an array or a comma-separated string. |
| event_external_id | string Your id for the event to schedule or reschedule after the edit. |
| event_external_id_new | string New external id for that event. |
| event_start_date | string Event start. A string with a UTC offset is an absolute
instant; one without is wall-clock time in
|
| event_type | string Event type id (see |
| event_time_zone | string ProLine time zone value, legacy option key or IANA name for a
naive |
| event_skip_external_create | string
|
{- "external_id": "CRM-10042",
- "contact_fname": "Dana",
- "contact_phone": "+15125550142",
- "project_name": "Ortiz re-roof",
- "project_address1": "12 Elm St",
- "project_city": "Austin",
- "project_state": "TX",
- "project_zip": "78701",
- "project_stage": "k97f2x1n0cq8vb3d5t6hm4ws2e7a9zq1",
- "project_tags": [
- "t44e9g7j2vq5nb1d8k3xm0wr6se2c5zb"
], - "event_start_date": "2026-10-15T14:00:00",
- "event_time_zone": "central",
- "event_type": "et4c8h2k6sq9nb3v1d7xm5wr0te4a8yc"
}{- "isBase64Encoded": false,
- "statusCode": 200,
- "statusDescription": "200 OK",
- "headers": {
- "Content-Type": "application/json"
}, - "body": {
- "status": "success",
- "response": {
- "status": "New project successfully created.",
- "project_id": "p12d8f6h4tq0nb2v5k9xm3wr7se1c4ya",
- "error": false,
- "main_contact_id": "k57a2c9d1xq4nb8v6m3wt5hs0e7r1pz2",
- "other_contact_1_id": "null",
- "other_contact_2_id": "null",
- "event_scheduled": "ev9f1j5m8xq3nb6v2d0km4wr7te1c9zd"
}
}
}Returns the stages of the company's project pipelines, grouped by pipeline
in pipeline order. The id is what /v1/find/project returns as
stage_id, and what project_stage takes on /v1/edit/project
(/v1/list/projects returns the stage name only). Archived stages are
not listed.
Response shape: direct (see Overview → Response shapes).
Empty, or {}. Any key is refused with 400.
{ }[- {
- "id": "k97f2x1n0cq8vb3d5t6hm4ws2e7a9zq1",
- "name": "New Lead",
- "label": "Sales | New Lead",
- "pipeline_id": "k170q3xw8zr5nc2vb4hd6tj9pm1ea8sy",
- "pipeline_name": "Sales"
}, - {
- "id": "k97f2x1n0cq8vb3d5t6hm4ws2e7a9zq2",
- "name": "Appointment Set",
- "label": "Sales | Appointment Set",
- "pipeline_id": "k170q3xw8zr5nc2vb4hd6tj9pm1ea8sy",
- "pipeline_name": "Sales"
}
]Pages through every job in the company, newest first, with its project,
service and stage. Filter by creation date with created_after /
created_before (both inclusive). Pages are limited to 100 rows and to an
offset of 10,000 rows; beyond that, or when a page would read more than the
per-call budget, the call answers 400 with a
{ "status": "error", "message": "..." } body and you should narrow the
date window. total is exact up to 4,000 matching rows and a lower bound
past that; has_more is authoritative. job_description is returned as
plain text.
Response shape: direct (see Overview → Response shapes).
| page | integer >= 1 Default: 1 1-based page number. |
| limit | integer [ 1 .. 100 ] Default: 100 Rows per page. A value above 100 is clamped to 100. |
| created_after | string or null <date-time> Only jobs created at or after this instant. A full ISO 8601 datetime with timezone (for example |
| created_before | string or null <date-time> Only jobs created at or before this instant. Same format as |
{- "limit": 100
}{- "page": 1,
- "limit": 100,
- "total": 3,
- "has_more": false,
- "results": [
- {
- "job_id": "jb1f2x1n0cq8vb3d5t6hm4ws2e7a9zq4",
- "job_name": "Tear-off and install",
- "job_description": "",
- "job_project_id": "k57abc1n0cq8vb3d5t6hm4ws2e7a9zq1",
- "job_service_id": "sv2f2x1n0cq8vb3d5t6hm4ws2e7a9zq5",
- "job_stage": "Scheduled",
- "job_last_status_change": "2026-09-10T12:00:00.000Z",
- "created_at": "2026-09-09T08:00:00.000Z",
- "updated_at": "2026-09-10T12:00:00.000Z"
}
]
}Looks up one contact by id, phone, email or address and returns the first
match. The keys are tried in that order: contact_id, then
contact_phone, then contact_email, then the address fields together;
the first key that matches wins and the rest are not consulted. Archived
contacts and contacts of other companies are not matched.
body is an array with one contact, or an empty array when nothing
matched (there is no not-found error). An empty request body returns the
empty array.
Response shape: wrapped (see Overview → Response shapes).
| contact_id | string (ProLineId) A ProLine record id. Opaque; in requests, a legacy id for the same record is also accepted. |
| contact_phone | string Any format; the number is normalized before matching. A saved contact is preferred over an unsaved draft with the same number. |
| contact_email | string Matched exactly. |
| contact_address1 | string Street line, used with |
| contact_address2 | string Accepted and ignored. |
| contact_city | string |
| contact_state | string |
| contact_zip | string |
{- "contact_phone": "+15125550142"
}{- "isBase64Encoded": false,
- "statusCode": 200,
- "statusDescription": "200 OK",
- "headers": {
- "Content-Type": "application/json"
}, - "body": [
- {
- "contact_id": "k57a2c9d1xq4nb8v6m3wt5hs0e7r1pz2",
- "first_name": "Dana",
- "last_name": "Ortiz",
- "contact_name": "Dana Ortiz",
- "display_name": "Dana Ortiz",
- "phone": "+15125550142",
- "email": "dana@example.com",
- "address1": "12 Elm St",
- "address2": "",
- "city": "Austin",
- "state": "TX",
- "zip": "78701",
- "contact_time_zone": "central",
- "assigned_to_id": "j97b4d2f8kq1nc5v7h3xm6tw9se0a2rz",
- "assigned_to_name": "Sam Rep",
- "assigned_to_proline": "j97b4d2f8kq1nc5v7h3xm6tw9se0a2rz",
- "assigned_to_email": "sam@roofer.example",
- "external_id": "",
- "notes": "",
- "lead_source": "m31c6e4g0rq2nd7v9j5zx8ty1sf3b4qw",
- "custom_field_1": "",
- "custom_field_2": "",
- "custom_field_3": ""
}
]
}Updates an existing contact, or creates one when no match is found. The
contact is matched by contact_id first; otherwise by contact_phone,
against the newest saved contact with the same number, then against an
unsaved draft with that number (the draft becomes the contact); otherwise
a new contact is created, which requires contact_fname.
Omitted or blank fields are left unchanged, so a value cannot be cleared
through this endpoint. external_id is stored on create only and ignored
on update.
Side effects: field changes are logged as activities, the company's contact webhooks fire, lead-source attribution on the contact's projects is synced, and a new contact is given the next contact number.
Rejections arrive inside body.response with error: true (the outer
status is still "success"): a missing first name on create, a phone
number already held by another contact (that contact's id is returned),
and others. When the company's automations are paused, body is
{ "ok": true } and nothing is written.
Response shape: wrapped (see Overview → Response shapes).
| contact_id | string (ProLineId) A ProLine record id. Opaque; in requests, a legacy id for the same record is also accepted. |
| contact_fname | string First name. Required when creating. |
| contact_lname | string |
| contact_organization | string Organization id. |
| contact_phone | string Any format; normalized to E.164. Also the lookup key when
|
| contact_email | string |
| contact_address1 | string |
| contact_address2 | string |
| contact_city | string |
| contact_state | string |
| contact_zip | string |
| contact_notes | string |
| contact_type | string Contact type id. Validated together with |
| lead_source | string Lead source id. Validated together with |
| time_zone | string ProLine time zone value ( |
| external_id | string Stored as the contact's legacy id on create only; ignored on update. |
| custom_field_1 | string Written to the first legacy custom field slot. Skipped when the company has no such field. |
| custom_field_2 | string |
| custom_field_3 | string |
| contact_tags | string Accepted and ignored. |
| contact_campaign_id | string Accepted and ignored. |
| custom_date_1 | string Accepted and ignored. |
| custom_date_2 | string Accepted and ignored. |
| custom_date_3 | string Accepted and ignored. |
{- "contact_fname": "Dana",
- "contact_lname": "Ortiz",
- "contact_phone": "512-555-0142",
- "contact_email": "dana@example.com",
- "contact_state": "TX",
- "custom_field_1": "Referral: HOA"
}{- "isBase64Encoded": false,
- "statusCode": 200,
- "statusDescription": "200 OK",
- "headers": {
- "Content-Type": "application/json"
}, - "body": {
- "status": "success",
- "response": {
- "status": "New contact successfully created.",
- "contact_id": "k57a2c9d1xq4nb8v6m3wt5hs0e7r1pz2",
- "error": false
}
}
}Pages through the company's quotes, newest first, with the project and
contact each belongs to, its sent and signed timestamps, totals and
deposit terms. Archived quotes are left out and are not counted in
total. Filter by creation date with created_after / created_before
(both inclusive). Pages are limited to 100 rows and to an offset of 10,000
rows; beyond that, or when a page would read more than the per-call
budget, the call answers 400 with a { "status": "error", "message": "..." }
body and you should narrow the date window. total is exact up to 4,000
matching rows and a lower bound past that; has_more is authoritative.
Response shape: direct (see Overview → Response shapes).
| page | integer >= 1 Default: 1 1-based page number. |
| limit | integer [ 1 .. 100 ] Default: 100 Rows per page. A value above 100 is clamped to 100. |
| created_after | string or null <date-time> Only quotes created at or after this instant. A full ISO 8601 datetime with timezone (for example |
| created_before | string or null <date-time> Only quotes created at or before this instant. Same format as |
{- "page": 2,
- "limit": 25,
- "created_before": "2026-10-01T00:00:00-07:00"
}{- "page": 2,
- "limit": 25,
- "total": 61,
- "has_more": true,
- "results": [
- {
- "quote_id": "q8xf2x1n0cq8vb3d5t6hm4ws2e7a9zq6",
- "quote_name": "Option A",
- "quote_project_id": "k57abc1n0cq8vb3d5t6hm4ws2e7a9zq1",
- "quote_contact_id": "c1xf2x1n0cq8vb3d5t6hm4ws2e7a9zq3",
- "quote_customer_name": "Pat Smith",
- "quote_sent_at": "2026-09-12T16:00:00.000Z",
- "quote_signing_completed_primary": null,
- "quote_signing_completed_all": null,
- "quote_subtotal": 18500,
- "quote_signed_total": null,
- "quote_deposit": 10,
- "quote_deposit_cap": null,
- "quote_signed_deposit": null,
- "quote_fixed_deposit_amount": null,
- "quote_profit_margin": 0.3,
- "quote_budget_planned_gross": 5920,
- "quote_cash_discount_dollar_value": null,
- "quote_cash_discount_margin": null,
- "quote_add_card_fee_to_margin": false,
- "quote_project_name": "Smith Re-roof",
- "quote_project_number": "P-1042",
- "quote_project_full_address": "12 Elm St, Reno, NV, 89501",
- "quote_contact_name": "Pat Smith",
- "quote_contact_number": "+17755550123",
- "quote_contact_organization": "",
- "quote_assignee_name": "Dana Reyes",
- "created_at": "2026-09-12T15:00:00.000Z",
- "updated_at": "2026-09-12T16:00:00.000Z"
}
]
}Pages through the company's orders (purchase and work orders), newest
first, with the project, quote, job and vendor each belongs to and its
signing status and dates. Filter by creation date with created_after /
created_before (both inclusive). Pages are limited to 100 rows and to an
offset of 10,000 rows; beyond that, or when a page would read more than the
per-call budget, the call answers 400 with a
{ "status": "error", "message": "..." } body and you should narrow the
date window. total is exact up to 4,000 matching rows and a lower bound
past that; has_more is authoritative. order_notes is returned as plain
text.
Response shape: direct (see Overview → Response shapes).
| page | integer >= 1 Default: 1 1-based page number. |
| limit | integer [ 1 .. 100 ] Default: 100 Rows per page. A value above 100 is clamped to 100. |
| created_after | string or null <date-time> Only orders created at or after this instant. A full ISO 8601 datetime with timezone (for example |
| created_before | string or null <date-time> Only orders created at or before this instant. Same format as |
{ }{- "page": 1,
- "limit": 100,
- "total": 1,
- "has_more": false,
- "results": [
- {
- "order_id": "or3f2x1n0cq8vb3d5t6hm4ws2e7a9zq7",
- "order_number": "PO-7",
- "order_name": "Shingles - Smith",
- "order_project_id": "k57abc1n0cq8vb3d5t6hm4ws2e7a9zq1",
- "order_quote_id": "q8xf2x1n0cq8vb3d5t6hm4ws2e7a9zq6",
- "order_job_id": "",
- "order_vendor_contact_id": "vc2f2x1n0cq8vb3d5t6hm4ws2e7a9zq8",
- "order_signing_status": "Sent",
- "order_signing_completed": null,
- "order_contractor_agreed": null,
- "order_sent_at": "2026-09-15T09:00:00.000Z",
- "order_target_date": "2026-09-22T00:00:00.000Z",
- "order_timing": "On Date",
- "order_total_value": null,
- "order_notes": "Deliver to side gate",
- "order_pdf": "",
- "created_at": "2026-09-15T08:30:00.000Z",
- "updated_at": "2026-09-15T09:00:00.000Z"
}
]
}Pages through the company's invoices, newest first, with totals, balance,
status dates and the billing, project and rep details captured on each.
Filter by creation date with created_after / created_before (both
inclusive). Pages are limited to 100 rows and to an offset of 10,000 rows;
beyond that, or when a page would read more than the per-call budget, the
call answers 400 with a { "status": "error", "message": "..." } body and
you should narrow the date window. total is exact up to 4,000 matching
rows and a lower bound past that; has_more is authoritative.
Response shape: direct (see Overview → Response shapes).
| page | integer >= 1 Default: 1 1-based page number. |
| limit | integer [ 1 .. 100 ] Default: 100 Rows per page. A value above 100 is clamped to 100. |
| created_after | string or null <date-time> Only invoices created at or after this instant. A full ISO 8601 datetime with timezone (for example |
| created_before | string or null <date-time> Only invoices created at or before this instant. Same format as |
{- "created_after": "2026-09-01T00:00:00Z",
- "created_before": "2026-09-30T23:59:59Z"
}{- "page": 1,
- "limit": 100,
- "total": 2,
- "has_more": false,
- "results": [
- {
- "invoice_id": "in4f2x1n0cq8vb3d5t6hm4ws2e7a9zq9",
- "invoice_number": 7,
- "invoice_name": "Deposit",
- "invoice_project_id": "k57abc1n0cq8vb3d5t6hm4ws2e7a9zq1",
- "invoice_status": "Sent",
- "invoice_due_date": "2026-09-20T00:00:00.000Z",
- "invoice_sent_at": "2026-09-13T10:00:00.000Z",
- "invoice_paid_in_full_at": null,
- "invoice_voided_at": null,
- "invoice_subtotal": 1850,
- "invoice_total": 1850,
- "invoice_amount_due": 1850,
- "invoice_remaining_balance": 1850,
- "invoice_cash_discount": 0,
- "invoice_service_taxes_dollar_value": 0,
- "invoice_last_payment_number": null,
- "invoice_billing_address1": "12 Elm St",
- "invoice_billing_address2": "",
- "invoice_billing_city": "Reno",
- "invoice_billing_state": "NV",
- "invoice_billing_zip": "89501",
- "invoice_country": "US",
- "invoice_customer_name": "Pat Smith",
- "invoice_customer_email": "pat@example.com",
- "invoice_customer_phone": "+17755550123",
- "invoice_customer_organization": "",
- "invoice_project_name": "Smith Re-roof",
- "invoice_project_address1": "12 Elm St",
- "invoice_project_address2": "",
- "invoice_project_city": "Reno",
- "invoice_project_state": "NV",
- "invoice_project_zip": "89501",
- "invoice_rep_name": "Dana Reyes",
- "invoice_rep_email": "dana@example.com",
- "invoice_rep_phone": "",
- "invoice_memo": "",
- "invoice_pdf": "",
- "created_at": "2026-09-13T09:00:00.000Z",
- "updated_at": "2026-09-13T10:00:00.000Z"
}
]
}Pages through the company's payments, newest first, with the project and
invoice each applies to, its status, method and amounts. Filter by
creation date with created_after / created_before (both inclusive).
Pages are limited to 100 rows and to an offset of 10,000 rows; beyond
that, or when a page would read more than the per-call budget, the call
answers 400 with a { "status": "error", "message": "..." } body and you
should narrow the date window. total is exact up to 4,000 matching rows
and a lower bound past that; has_more is authoritative.
Response shape: direct (see Overview → Response shapes).
| page | integer >= 1 Default: 1 1-based page number. |
| limit | integer [ 1 .. 100 ] Default: 100 Rows per page. A value above 100 is clamped to 100. |
| created_after | string or null <date-time> Only payments created at or after this instant. A full ISO 8601 datetime with timezone (for example |
| created_before | string or null <date-time> Only payments created at or before this instant. Same format as |
{- "limit": 10
}{- "page": 1,
- "limit": 10,
- "total": 1,
- "has_more": false,
- "results": [
- {
- "payment_id": "py5f2x1n0cq8vb3d5t6hm4ws2e7a9zr1",
- "payment_number": 3,
- "payment_project_id": "k57abc1n0cq8vb3d5t6hm4ws2e7a9zq1",
- "payment_invoice_id": "in4f2x1n0cq8vb3d5t6hm4ws2e7a9zq9",
- "payment_invoice_number": 7,
- "payment_status": "Complete",
- "payment_status_info": null,
- "payment_date": "2026-09-14T17:00:00.000Z",
- "payment_refusal_description": "",
- "payment_type": "Card",
- "payment_total": 1850,
- "payment_invoice_revenue": 1850,
- "payment_net_revenue": 1795.5,
- "payment_description": "Deposit",
- "created_at": "2026-09-14T17:00:00.000Z",
- "updated_at": "2026-09-14T17:00:05.000Z"
}
]
}Returns the open appointment start times for one or more team members
between start_date and end_date, one row per team member. Start
times come back as a single comma-separated string in the time zone of
the company's default location, with no UTC offset. The window may span
at most 30 days.
The dates are read as UTC when they carry no offset. When user_id,
start_date or end_date is missing, body is [{ "start_times": "" }]
rather than an error. A window over 30 days, or a calendar that could not
be read, answers a { "status": "...", "error": true } object in body;
an unknown team member, event type or date format answers the backend
validation error in body. When the company's automations are paused,
body is { "ok": true }.
Response shape: wrapped (see Overview → Response shapes).
| user_id required | Array of strings or string One team member id, or an array of them. Each is echoed back as sent. An unknown id answers the validation error. |
| start_date required | string Start of the window; ISO 8601, UTC when no offset is given. |
| end_date required | string End of the window; at most 30 days after |
| event_type | string Event type id (see |
{- "user_id": "j97b4d2f8kq1nc5v7h3xm6tw9se0a2rz",
- "start_date": "2026-10-15T00:00:00Z",
- "end_date": "2026-10-17T00:00:00Z",
- "event_type": "et4c8h2k6sq9nb3v1d7xm5wr0te4a8yc"
}{- "isBase64Encoded": false,
- "statusCode": 200,
- "statusDescription": "200 OK",
- "headers": {
- "Content-Type": "application/json"
}, - "body": [
- {
- "user_id": "j97b4d2f8kq1nc5v7h3xm6tw9se0a2rz",
- "start_times": "2026-10-15T09:00:00, 2026-10-15T09:30:00, 2026-10-15T13:00:00"
}
]
}Reschedules an existing event, or creates one on a project. When
event_id names a live event it is updated; otherwise a new event is
created, which requires project_id.
start_date is required on every call. The end time is derived from the
event type's default duration. The event type is event_type, else the
existing event's type, else the company's first event type; the calendar
is the existing event's, else the type's default, else the company's
first calendar. The event title is always set to the event type name.
Omitted or blank fields are left unchanged, so a value cannot be cleared
through this endpoint. An assignee that does not resolve is dropped and
the existing assignee kept.
Side effects: a new event gets the project's main-contact email as a
participant; event webhooks fire. A missing or unparseable start_date,
or a company with no event types, answers the backend validation error
in body. Creating without project_id, or with no calendar available,
answers a { "status": "...", "error": true } object in body. When the
company's automations are paused, body is { "ok": true } and nothing
is written.
Response shape: wrapped (see Overview → Response shapes).
| start_date required | string New start. A string with a UTC offset is an absolute
instant; one without is wall-clock time in |
| time_zone | string ProLine time zone value ( |
| event_id | string Event to update; a legacy id is also accepted. Cancelled events do not match. |
| project_id | string Project for a new event; a legacy id is also accepted. Required when creating. |
| event_type | string Event type id (see |
| assignee | string Team member id. |
| external_id | string Your own id for the event, stored on it. |
| external_reschedule_link | string A URL stored on the event for rescheduling it in your system. |
| skip_external_create | string
|
{- "project_id": "p12d8f6h4tq0nb2v5k9xm3wr7se1c4ya",
- "start_date": "2026-10-15T14:00:00",
- "time_zone": "central",
- "event_type": "et4c8h2k6sq9nb3v1d7xm5wr0te4a8yc",
- "assignee": "j97b4d2f8kq1nc5v7h3xm6tw9se0a2rz",
- "external_id": "cal-88213"
}{- "isBase64Encoded": false,
- "statusCode": 200,
- "statusDescription": "200 OK",
- "headers": {
- "Content-Type": "application/json"
}, - "body": {
- "status": "New event successfully edited.",
- "event_id": "ev9f1j5m8xq3nb6v2d0km4wr7te1c9zd",
- "error": false
}
}Cancels the event named by event_id. The event is marked cancelled, not
deleted; /v1/find/event still returns it by event_id or
external_id, with cancelled: true. Cancelling an already cancelled
event runs the cancellation again and answers success. An event that
came from an external calendar cannot be cancelled here; the call fails
inside body.
A missing or unknown event_id answers { "status": "No existing event found.", "error": true } in body. When the company's automations are
paused, body is { "ok": true } and nothing is written.
Response shape: wrapped (see Overview → Response shapes).
| event_id required | string (ProLineId) A ProLine record id. Opaque; in requests, a legacy id for the same record is also accepted. |
{- "event_id": "ev9f1j5m8xq3nb6v2d0km4wr7te1c9zd"
}{- "isBase64Encoded": false,
- "statusCode": 200,
- "statusDescription": "200 OK",
- "headers": {
- "Content-Type": "application/json"
}, - "body": {
- "status": "Event successfully cancelled",
- "error": false
}
}Looks up one event and returns the first match. The keys are tried in this order:
event_id: exact lookup; a cancelled event is returned, with
cancelled: true.external_id: exact lookup; a cancelled event is returned.project_id: the first live event on that project, within the day
of start_date when given, otherwise within the next 365 days.
event_type and assignee narrow the search.start_date alone: the first live event on that day, with the same
filters, and only for days from today on.Exact lookups ignore the filters. Date searches skip cancelled events.
start_date on the two date searches is read as a plain date; the day
window is taken in the server's clock, so an event near midnight may
fall on the neighbouring day.
body is an array with one event, or an empty array when nothing
matched (there is no not-found error). A project_id, event_type or
assignee that does not resolve answers [].
Response shape: wrapped (see Overview → Response shapes).
| event_id | string (ProLineId) A ProLine record id. Opaque; in requests, a legacy id for the same record is also accepted. |
| external_id | string The |
| project_id | string Project id; a legacy id is also accepted. |
| start_date | string A date ( |
| event_type | string Event type id. Filter for the date searches. |
| assignee | string Team member id. Filter for the date searches. |
{- "project_id": "p12d8f6h4tq0nb2v5k9xm3wr7se1c4ya",
- "start_date": "2026-10-15"
}{- "isBase64Encoded": false,
- "statusCode": 200,
- "statusDescription": "200 OK",
- "headers": {
- "Content-Type": "application/json"
}, - "body": [
- {
- "event_id": "ev9f1j5m8xq3nb6v2d0km4wr7te1c9zd",
- "external_id": "cal-88213",
- "cancelled": false,
- "cancelled_at": null,
- "type": "Inspection",
- "type_id": "et4c8h2k6sq9nb3v1d7xm5wr0te4a8yc",
- "duration": 60,
- "start_date": "2026-10-15T19:00:00.000Z",
- "end_date": "2026-10-15T20:00:00.000Z",
- "time_zone": "central",
- "assignee_name": "Sam Rep",
- "assignee_email": "sam@roofer.example",
- "assignee_proline": "j97b4d2f8kq1nc5v7h3xm6tw9se0a2rz",
- "assignee_id": "j97b4d2f8kq1nc5v7h3xm6tw9se0a2rz",
- "project_name": "Ortiz re-roof",
- "project_address1": "12 Elm St",
- "project_address2": null,
- "project_city": "Austin",
- "project_state": "TX",
- "project_zip": "78701",
- "project_id": "p12d8f6h4tq0nb2v5k9xm3wr7se1c4ya",
- "contact_name": "Dana Ortiz",
- "contact_email": "dana@example.com",
- "contact_phone": "+15125550142",
- "contact_time_zone": "central",
- "contact_id": "k57a2c9d1xq4nb8v6m3wt5hs0e7r1pz2"
}
]
}Returns every event type in the company, the default type first and the
rest by name A to Z. There is no paging and no envelope. name and
label carry the same value. The id is what event endpoints take as the
event type.
Response shape: direct (see Overview → Response shapes).
Empty, or {}. Any key is refused with 400.
{ }[- {
- "label": "Inspection",
- "name": "Inspection",
- "id": "et1f2x1n0cq8vb3d5t6hm4ws2e7a9zr6"
}, - {
- "label": "Install",
- "name": "Install",
- "id": "et2f2x1n0cq8vb3d5t6hm4ws2e7a9zr7"
}
]Pages through the activity feed of one project, contact or organization,
newest first, optionally restricted to a set of activity types. Exactly
one of project_id, contact_id or org_id is required. A parent that
does not exist in the company gives an empty page with total: 0, not an
error. Filter by creation date with created_after / created_before
(both inclusive).
Each call may read at most about 12,000 activity records or 10 MB; when a
page cannot be served within that, or the offset is past 10,000 rows, the
call answers 400 with a { "status": "error", "message": "..." } body.
For a project or organization the whole feed is read and the date window
applied afterwards, so on a very busy project reduce limit as well as
narrowing the window. total is exact up to 4,000 matching rows and a
lower bound past that; has_more is authoritative.
Call rows omit the transcript segments; /v1/find/activity returns them.
Ids on activities of migrated companies may be legacy ids (see Overview →
Identifiers).
Response shape: direct (see Overview → Response shapes).
| project_id | string (ProLineId) A ProLine record id. Opaque; in requests, a legacy id for the same record is also accepted. |
| contact_id | string (ProLineId) A ProLine record id. Opaque; in requests, a legacy id for the same record is also accepted. |
| org_id | string (ProLineId) A ProLine record id. Opaque; in requests, a legacy id for the same record is also accepted. |
| type | Array of strings Activity type names to include, for example |
| page | integer >= 1 Default: 1 1-based page number. |
| limit | integer [ 1 .. 100 ] Default: 100 Rows per page. A value above 100 is clamped to 100. |
| created_after | string or null <date-time> Only activities created at or after this instant. A full ISO 8601 datetime with timezone (for example |
| created_before | string or null <date-time> Only activities created at or before this instant. Same format as |
{- "project_id": "k57abc1n0cq8vb3d5t6hm4ws2e7a9zq1",
- "type": [
- "call_created",
- "note_created"
], - "limit": 20
}{- "page": 1,
- "limit": 20,
- "total": 7,
- "has_more": false,
- "results": [
- {
- "activity_id": "1718301234567x123456789012345678",
- "type": "call_created",
- "created_at": "2026-09-11T14:02:00.000Z",
- "title": "Outbound call",
- "description": null,
- "notes": null,
- "project_id": "1718300000000x111111111111111111",
- "project_ids": [
- "1718300000000x111111111111111111"
], - "org_ids": [ ],
- "contact_id": "1718300000000x222222222222222222",
- "created_by": {
- "user_id": "1718200000000x333333333333333333",
- "name": "Dana Reyes",
- "email": "dana@example.com"
}, - "call": {
- "call_id": "ca9f2x1n0cq8vb3d5t6hm4ws2e7a9zr2",
- "direction": "outbound",
- "status": "completed",
- "duration": 184,
- "transcript_complete": true,
- "transcript_summary": "Confirmed Tuesday install.",
- "call_score": 8
}
}
]
}Returns one activity by id, in the same shape as a /v1/list/activities
row. For a call activity the transcript segments are included under
call.transcript. The id may be a ProLine id or a legacy id.
When no activity with that id is visible to the company the call still
answers 200, with an empty array [] as the body, not 404. An activity
whose detail is larger than the response limit answers 400.
Response shape: direct (see Overview → Response shapes).
| activity_id required | string non-empty The activity's id (ProLine or legacy). Surrounding whitespace is trimmed. |
{- "activity_id": "1718301234567x123456789012345678"
}{- "activity_id": "1718301234567x123456789012345678",
- "type": "call_created",
- "created_at": "2026-09-11T14:02:00.000Z",
- "title": "Outbound call",
- "description": null,
- "notes": null,
- "project_id": "1718300000000x111111111111111111",
- "project_ids": [
- "1718300000000x111111111111111111"
], - "org_ids": [ ],
- "contact_id": "1718300000000x222222222222222222",
- "created_by": {
- "user_id": "1718200000000x333333333333333333",
- "name": "Dana Reyes",
- "email": "dana@example.com"
}, - "call": {
- "call_id": "ca9f2x1n0cq8vb3d5t6hm4ws2e7a9zr2",
- "direction": "outbound",
- "status": "completed",
- "duration": 184,
- "transcript_complete": true,
- "transcript_summary": "Confirmed Tuesday install.",
- "call_score": 8,
- "transcript": [
- {
- "speaker": "agent",
- "text": "Hi Pat, this is Dana from the roofing team.",
- "start": 0.4,
- "end": 2.1
}, - {
- "speaker": "customer",
- "text": "Hi Dana, Tuesday works for us.",
- "start": 2.5,
- "end": 4
}
]
}
}Creates an "alert" activity, titled alert_text, on a contact and/or a
project. At least one of contact_id or project_id is required. One
activity is created per project involved: the project you name plus the
project linked to the contact, so a contact with a linked project yields
two. A contact with no project gets a single contact-only activity.
Nothing is deduplicated.
The answer's activity_created is the number of activities created, as a
string; it is not an id (the call and message endpoints return an id
under the same key).
Response shape: wrapped (see Overview → Response shapes).
| alert_text required | string Activity title. Required by the backend. |
| alert_extended | string Activity description. |
| contact_id | string Contact to alert on, by ProLine or legacy id. One of |
| project_id | string Project to alert on, by ProLine or legacy id. One of |
{- "contact_id": "j97f2x1n0cq8vb3d5t6hm4ws2e7a9zq1",
- "alert_text": "Insurance adjuster scheduled",
- "alert_extended": "Adjuster visit Tuesday 10am"
}{- "isBase64Encoded": false,
- "statusCode": 200,
- "statusDescription": "200 OK",
- "headers": {
- "Set-cookie": "cookies",
- "Content-Type": "application/json"
}, - "body": {
- "activity_created": "2"
}
}Logs a phone call with a contact: a call record is stored and a call
activity is created from it. contact and type are required by the
backend. The call is outbound when type is "outgoing" and inbound for
any other value; a voicemail URL marks it as a voicemail.
Dedupe is by external_id: when a call with that external id already
exists, nothing is created and body is { "error": "A call already exists with that external ID." }. An unknown contact answers the same
way with "No contact could be found for that ID." Without external_id,
repeated sends log the call again.
Response shape: wrapped (see Overview → Response shapes).
| contact required | string The contact, by ProLine or legacy id. Required by the backend. |
| type required | string
|
| user | string The team member on the call, by ProLine or legacy id. An unknown user is ignored. |
| duration | string Call length in seconds, as a numeric string. |
| recording | string <uri> URL of the call recording. |
| voicemail | string <uri> URL of the voicemail. When present the call is logged as a voicemail. |
| external_id | string Your id for the call. Dedupe key; a repeat is refused. |
| call_note | string Note on the call. |
{- "contact": "j97f2x1n0cq8vb3d5t6hm4ws2e7a9zq1",
- "type": "incoming",
- "duration": "312",
- "external_id": "ringcentral-88211",
- "call_note": "Asked for a Saturday inspection"
}{- "isBase64Encoded": false,
- "statusCode": 200,
- "statusDescription": "200 OK",
- "headers": {
- "Set-cookie": "cookies",
- "Content-Type": "application/json"
}, - "body": {
- "activity_created": "a1bf2x1n0cq8vb3d5t6hm4ws2e7a9zq1"
}
}Logs a text message exchanged with a contact: a message record is stored,
attached to the contact's latest project, and a message-sent or
message-received activity is created from it. contact, type and
message_body are required by the backend. The message is outbound when
type is "outgoing" and inbound for any other value.
Dedupe is by external_id: when a message with that external id already
exists, nothing is created and body is { "error": "A message already exists with that external ID." }. An unknown contact answers the same
way with "No contact could be found for that ID." Without external_id,
repeated sends log the message again.
Response shape: wrapped (see Overview → Response shapes).
| contact required | string The contact, by ProLine or legacy id. Required by the backend. |
| type required | string
|
| message_body required | string The message text. Required by the backend. |
| message_note | string Note on the message. |
| user | string The team member who sent or received it, by ProLine or legacy id. |
| external_id | string Your id for the message. Dedupe key; a repeat is refused. |
{- "contact": "j97f2x1n0cq8vb3d5t6hm4ws2e7a9zq1",
- "type": "outgoing",
- "message_body": "Your crew arrives Thursday at 8am.",
- "external_id": "twilio-SM9f3a1c"
}{- "isBase64Encoded": false,
- "statusCode": 200,
- "statusDescription": "200 OK",
- "headers": {
- "Set-cookie": "cookies",
- "Content-Type": "application/json"
}, - "body": {
- "activity_created": "a1bf2x1n0cq8vb3d5t6hm4ws2e7a9zq2"
}
}Pages through the files of one project, newest first, with each file's
download URL, folder and photo details. project_id is required.
Restrict the listing to one folder with folder_id (null for the files
in no folder), to a media type with mime_type_prefix (for example
image/), and to a creation window with created_after /
created_before (both inclusive). Archived folders and the files in them
are left out. A project or folder that does not exist in the company gives
an empty page, not an error.
A project with more than 2,000 folders cannot be listed whole: the call
answers 400 and you pass folder_id to list one folder at a time. Pages
are limited to 100 rows and to an offset of 10,000 rows; beyond that, or
when a page would read more than the per-call budget, the call answers 400
with a { "status": "error", "message": "..." } body. total is exact up
to 4,000 matching rows and a lower bound past that; has_more is
authoritative.
Response shape: direct (see Overview → Response shapes).
| project_id required | string (ProLineId) A ProLine record id. Opaque; in requests, a legacy id for the same record is also accepted. |
| folder_id | string or null non-empty Only files directly in this folder. |
| mime_type_prefix | string or null non-empty Only files whose media type starts with this prefix, for example |
| page | integer >= 1 Default: 1 1-based page number. |
| limit | integer [ 1 .. 100 ] Default: 100 Rows per page. A value above 100 is clamped to 100. |
| created_after | string or null <date-time> Only files created at or after this instant. A full ISO 8601 datetime with timezone (for example |
| created_before | string or null <date-time> Only files created at or before this instant. Same format as |
{- "project_id": "k57abc1n0cq8vb3d5t6hm4ws2e7a9zq1",
- "folder_id": null,
- "mime_type_prefix": "image/",
- "limit": 50
}{- "page": 1,
- "limit": 50,
- "total": 4,
- "has_more": false,
- "results": [
- {
- "file_id": "fl6f2x1n0cq8vb3d5t6hm4ws2e7a9zr3",
- "project_id": "k57abc1n0cq8vb3d5t6hm4ws2e7a9zq1",
- "filename": "front-elevation.jpg",
- "mime_type": "image/jpeg",
- "size": 2048311,
- "photo_type": "Before",
- "notes": null,
- "folder_id": null,
- "folder_name": null,
- "captured_at": "2026-09-05T18:21:00.000Z",
- "created_at": "2026-09-05T18:25:00.000Z",
- "updated_at": "2026-09-05T18:25:00.000Z"
}
]
}Pages through the folders of one project. project_id is required. With
parent_folder_id the direct children of that folder are returned
(null for the top level); without it every live folder is returned in
tree order, each folder followed by its descendants. Rows are in tree
order, not newest first. Archived folders are left out. A project or
folder that does not exist in the company gives an empty page, not an
error.
This endpoint takes no date filters: created_after and created_before
are refused with 400 like any other unknown key. A project with more than
2,000 folders cannot be listed whole: the call answers 400 and you pass
parent_folder_id to list one level at a time. Pages are limited to 100
rows and to an offset of 10,000 rows. total is exact up to 4,000
matching rows and a lower bound past that; has_more is authoritative.
Response shape: direct (see Overview → Response shapes).
| project_id required | string (ProLineId) A ProLine record id. Opaque; in requests, a legacy id for the same record is also accepted. |
| parent_folder_id | string or null non-empty Only the direct children of this folder. |
| page | integer >= 1 Default: 1 1-based page number. |
| limit | integer [ 1 .. 100 ] Default: 100 Rows per page. A value above 100 is clamped to 100. |
{- "project_id": "k57abc1n0cq8vb3d5t6hm4ws2e7a9zq1",
- "parent_folder_id": null
}{- "page": 1,
- "limit": 100,
- "total": 3,
- "has_more": false,
- "results": [
- {
- "folder_id": "fo7f2x1n0cq8vb3d5t6hm4ws2e7a9zr4",
- "project_id": "k57abc1n0cq8vb3d5t6hm4ws2e7a9zq1",
- "name": "Photos",
- "parent_folder_id": null,
- "order": 0,
- "created_at": "2026-09-02T15:04:06.000Z",
- "updated_at": "2026-09-02T15:04:06.000Z"
}, - {
- "folder_id": "fo8f2x1n0cq8vb3d5t6hm4ws2e7a9zr5",
- "project_id": "k57abc1n0cq8vb3d5t6hm4ws2e7a9zq1",
- "name": "Documents",
- "parent_folder_id": null,
- "order": 1,
- "created_at": "2026-09-02T15:04:06.000Z",
- "updated_at": "2026-09-02T15:04:06.000Z"
}
]
}Downloads the file at file_url and attaches it to the project named by
project_id. It is the same storage path as /v1/import/file, but the
project is given by id only; use the import endpoint to target a project
by external id or through a contact. Nothing is deduplicated: sending the
same URL twice stores two files.
project_id and file_url are required by the backend. The URL is
probed before download; the file may be at most 50,000,000 bytes (50 MB).
When the probe fails or reports a larger size, body is an error object
with statusCode: 400 and a message beginning "Temporary error
connecting to CloudFlare Functions - Get File Size", rather than the usual
outcome shape. The URL must be publicly reachable.
This endpoint needs the import permission on your partner key, as the
/v1/import/* paths do, not a general one.
Values are sent as strings; arrays are flattened to comma-separated text.
Response shape: wrapped (see Overview → Response shapes).
| project_id required | string Target project, by ProLine or legacy id. Required by the backend. |
| file_url required | string <uri> Public URL to download. Required by the backend. |
| file_name | string Name to store the file under, truncated to 100 characters. Defaults to the file name in the URL. |
{- "project_id": "k57f2x1n0cq8vb3d5t6hm4ws2e7a9zq1",
- "file_name": "Roof north side.jpg"
}{- "isBase64Encoded": false,
- "statusCode": 200,
- "statusDescription": "200 OK",
- "headers": {
- "Set-cookie": "cookies",
- "Content-Type": "application/json"
}, - "body": {
- "status": "success",
- "response": {
- "project_id": "k57f2x1n0cq8vb3d5t6hm4ws2e7a9zq1",
- "project_name": "Whitfield re-roof",
- "file_id": "f31f2x1n0cq8vb3d5t6hm4ws2e7a9zq2",
- "file_name": "Roof north side.jpg"
}
}
}Creates one project from an external system, and its main contact when
no existing contact matches. The call is idempotent on
project_external_id: when a project with that external id (or the
project named by proline_id) already exists, nothing is created and the
answer carries duplicate: true with the existing ids. The duplicate
message always names the external id, even when proline_id matched.
The contact is found by contact_id, then contact_external_id, then
contact_email, then contact_phone. When none matches, a contact is
created from the contact_* fields; at least one of first name, last
name, email or phone must be supplied, or the import fails. Lookup values
such as stage, lead_source and assignee take a ProLine id, a legacy
id or the record's name; an unresolved value is skipped, except stage,
which falls back to the first stage of the company's project pipeline.
Side effects: a created contact gets its default folders, field-change activities and the contact-created webhook; the project is queued for accounting sync, address geocoding and stage workflows.
Response shape: wrapped (see Overview → Response shapes).
| project_external_id | string Your id for the project. Idempotency key; a repeat is skipped with |
| proline_id | string An existing project's ProLine or legacy id. When found, the call is skipped as a duplicate. |
| contact_id | string Existing contact to attach, by ProLine or legacy id. |
| contact_external_id | string Existing contact to attach, by the external id given at contact import. |
| contact_email | string Finds an existing contact by email; otherwise the email of the created contact. |
| contact_phone | string Finds an existing contact by phone; otherwise the phone of the created contact. |
| contact_first_name | string First name of the contact to create. |
| contact_last_name | string Last name of the contact to create. |
| contact_type | string Contact type, by ProLine id, legacy id or name. Unresolved values are skipped. |
| lead_source | string Lead source, by ProLine id, legacy id or name. Unresolved values are skipped. |
string or Array of strings Tags for the contact, as an array or a comma-separated string; trimmed and deduplicated. Unknown tags are skipped, not created. | |
| project_name | string Project name. Defaults to a name built from the contact. |
| project_number | string Project number. When given it is used as-is and the company's counter moves past it; otherwise the next number is allocated. |
| stage | string Project stage, by ProLine id, legacy id or name. Unresolved values fall back to the company's first project stage; a company with no project stage fails the import. |
| location | string Location, by ProLine id, legacy id or name. Unresolved values are skipped. |
| area | string Area, by ProLine id, legacy id or name. Unresolved values are skipped. |
| assignee | string Assigned team member, by ProLine id, legacy id or name. Unresolved values are skipped. |
| category | string Project category, by ProLine id, legacy id or name. Unresolved values are skipped. |
| type | string Project type, by ProLine id, legacy id or name. Unresolved values are skipped. |
string or Array of strings Services, as an array or a comma-separated string of ids, legacy ids or names. | |
string or Array of strings Project tags, as an array or a comma-separated string. Unknown tags are skipped, not created. | |
| address1 | string |
| address2 | string |
| city | string |
| state | string |
| zip | string |
| notes | string Project notes. When |
| custom_field_1 | string Written to the company's first legacy custom-field slot. Companies without legacy slots skip it. |
| custom_field_2 | string As |
| custom_field_3 | string As |
| custom_field_4 | string As |
| custom_field_5 | string As |
| custom_field_6 | string As |
| custom_field_7 | string As |
| revenue | string Expected revenue as a numeric string. Non-numeric values are skipped. |
| cost | string Not stored as a budget; appended to |
| lead_date | string Date the project became a lead. ISO 8601 or |
| inspection_date | string As |
| open_date | string As |
| won_date | string As |
| completed_date | string As |
| closed_date | string As |
| disqualified_date | string As |
| lost_date | string As |
{- "project_external_id": "crm-7781",
- "contact_first_name": "Dana",
- "contact_last_name": "Whitfield",
- "contact_email": "dana.w@example.com",
- "contact_phone": "(555) 010-2244",
- "project_name": "Whitfield re-roof",
- "stage": "New Lead",
- "lead_source": "Angi",
- "address1": "14 Juniper Ct",
- "city": "Boise",
- "state": "ID",
- "zip": "83702",
- "services": [
- "Roof Replacement"
], - "revenue": "18500",
- "lead_date": "2026-09-30"
}{- "isBase64Encoded": false,
- "statusCode": 200,
- "statusDescription": "200 OK",
- "headers": {
- "Set-cookie": "cookies",
- "Content-Type": "application/json"
}, - "body": {
- "status": "success",
- "response": {
- "status": "New project successfully created.",
- "error": false,
- "project_id": "k57f2x1n0cq8vb3d5t6hm4ws2e7a9zq1",
- "project_number": "1042",
- "contact_id": "j97f2x1n0cq8vb3d5t6hm4ws2e7a9zq1"
}
}
}Creates one contact from an external system. The call is idempotent on
external_id only: when a contact with that external id already exists,
nothing is created and the answer carries duplicate: true with the
existing ids. There is no matching by email or phone, so repeated sends
without external_id create duplicate contacts.
At least one of first_name, last_name, email or phone is required,
or the import fails. The phone is normalised to E.164 (digits only as a
fallback) and dropped when too short. Lookup values (assigned,
contact_type, lead_source, organization, contact_tags) take a
ProLine id, a legacy id or the record's name; an unresolved value is
skipped, never created.
Side effects: the contact gets its default folders, field-change activities and the contact-created webhook.
Response shape: wrapped (see Overview → Response shapes).
| external_id | string Your id for the contact. Idempotency key; a repeat is skipped with |
| first_name | string |
| last_name | string |
string | |
| phone | string Normalised to E.164; dropped when too short to be a phone number. |
| organization | string Organization, by ProLine id, legacy id or name. Unresolved values are skipped. |
| address1 | string |
| address2 | string |
| city | string |
| state | string |
| zip | string |
| assigned | string Assigned team member, by ProLine id, legacy id or name. Unresolved values are skipped. |
| contact_type | string Contact type, by ProLine id, legacy id or name. Unresolved values are skipped. |
| lead_source | string Lead source, by ProLine id, legacy id or name. Unresolved values are skipped. |
string or Array of strings Tags, as an array or a comma-separated string; trimmed and deduplicated. Unknown tags are skipped, not created. | |
| notes | string |
| custom_field_1 | string Written to the company's first legacy custom-field slot. Companies without legacy slots skip it. |
| custom_field_2 | string As |
| custom_field_3 | string As |
{- "external_id": "hs-20931",
- "first_name": "Marcus",
- "last_name": "Oyelaran",
- "email": "marcus@example.net",
- "phone": "5550198877",
- "contact_type": "Homeowner",
- "lead_source": "Website",
- "contact_tags": "referral,spring-promo",
- "city": "Tulsa",
- "state": "OK"
}{- "isBase64Encoded": false,
- "statusCode": 200,
- "statusDescription": "200 OK",
- "headers": {
- "Set-cookie": "cookies",
- "Content-Type": "application/json"
}, - "body": {
- "status": "success",
- "response": {
- "status": "New contact successfully created.",
- "error": false,
- "contact_id": "j97f2x1n0cq8vb3d5t6hm4ws2e7a9zq1",
- "contact_number": "317"
}
}
}Logs a historical activity (a call, message, note or email) against a
contact and/or a project. One activity record is created; no call or
message record is, unlike /v1/activity/create_call and
/v1/activity/create_message. Nothing is deduplicated: sending the same
activity twice logs it twice.
activity_type and date must be present or the request is refused with
400 before it reaches the backend. At least one of contact_id,
contact_external_id, project_id or project_external_id must resolve,
or the import fails. The activity's title is subject, else note, else
"Imported body, else note when a
subject was given.
Values are sent as strings; arrays are flattened to comma-separated text.
Response shape: wrapped (see Overview → Response shapes).
| activity_type required | string Case-insensitive. |
| date required | string When the activity happened. ISO 8601 or |
| subject | string Activity title. |
| body | string Activity description. |
| note | string Fallback for the title when |
| user | string The team member who performed the activity, by ProLine id, legacy id or name. |
| voicemail | string Voicemail URL. Stored on the activity only; no call record is created. |
| recording | string Recording URL. Stored on the activity only. |
| duration | string Duration in seconds, as a string. Stored on the activity only. |
| contact_id | string Contact to log against, by ProLine or legacy id. |
| contact_external_id | string Contact to log against, by the external id given at contact import. |
| project_id | string Project to log against, by ProLine or legacy id. |
| project_external_id | string Project to log against, by the external id given at project import. |
{- "activity_type": "call",
- "date": "2026-09-28T15:30:00-06:00",
- "contact_external_id": "hs-20931",
- "subject": "Follow-up call",
- "body": "Discussed shingle colors",
- "duration": "420",
- "user": "sales@example.com"
}{- "isBase64Encoded": false,
- "statusCode": 200,
- "statusDescription": "200 OK",
- "headers": {
- "Set-cookie": "cookies",
- "Content-Type": "application/json"
}, - "body": {
- "status": "success",
- "response": {
- "status": "Activity successfully created.",
- "error": false,
- "activity_id": "a1bf2x1n0cq8vb3d5t6hm4ws2e7a9zq1"
}
}
}Downloads the file at file_url and attaches it to a project. The
project is found by project_id or project_external_id; failing those,
by contact_id or contact_external_id, in which case the contact's
newest project is used. A contact with no project fails the import.
file_name and file_url must be present, along with at least one of
the four reference keys, or the request is refused with 400 before it
reaches the backend. The URL must be publicly reachable. The file may be
at most 50,000,000 bytes (50 MB); larger files fail the import. Nothing
is deduplicated: sending the same URL twice stores two files.
Values are sent as strings; arrays are flattened to comma-separated text.
Response shape: wrapped (see Overview → Response shapes).
| file_name required | string Name to store the file under, truncated to 100 characters. The key must be present; a blank value falls back to the file name in the URL. |
| file_url required | string <uri> Public URL to download. A blank value fails the import. |
| project_id | string Target project, by ProLine or legacy id. |
| project_external_id | string Target project, by the external id given at project import. |
| contact_id | string Contact whose newest project is the target, by ProLine or legacy id. Used only when no project key is given. |
| contact_external_id | string Contact whose newest project is the target, by the external id given at contact import. Used only when no project key is given. |
{- "project_external_id": "crm-7781",
- "file_name": "signed-contract.pdf",
}{- "isBase64Encoded": false,
- "statusCode": 200,
- "statusDescription": "200 OK",
- "headers": {
- "Set-cookie": "cookies",
- "Content-Type": "application/json"
}, - "body": {
- "status": "success",
- "response": {
- "project_id": "k57f2x1n0cq8vb3d5t6hm4ws2e7a9zq1",
- "project_name": "Whitfield re-roof",
- "file_id": "f31f2x1n0cq8vb3d5t6hm4ws2e7a9zq1",
- "file_name": "signed-contract.pdf"
}
}
}Creates taxonomy values (contact types, tags, lead sources, project types,
categories, services and areas) by name, so that later imports can refer
to them by name. Each key takes an array or a comma-separated string of
names. A name that already exists in the company (or matches a legacy id)
is left alone and listed under ignored; the others are created and
listed under created. {} is accepted and answers with empty created
and ignored objects.
Response shape: wrapped (see Overview → Response shapes).
string or Array of strings (NameList) | |
string or Array of strings (NameList) | |
string or Array of strings (NameList) | |
string or Array of strings (NameList) | |
string or Array of strings (NameList) | |
string or Array of strings (NameList) | |
string or Array of strings (NameList) | |
string or Array of strings (NameList) |
{- "lead_sources": [
- "Angi",
- "Door knock"
], - "project_services": "Roof Replacement, Gutters"
}{- "isBase64Encoded": false,
- "statusCode": 200,
- "statusDescription": "200 OK",
- "headers": {
- "Set-cookie": "cookies",
- "Content-Type": "application/json"
}, - "body": {
- "status": "success",
- "response": {
- "status": "Tags import complete.",
- "error": false,
- "created": {
- "lead_sources": [
- "Door knock"
], - "project_services": [
- "Gutters"
]
}, - "ignored": {
- "lead_sources": [
- "Angi"
], - "project_services": [
- "Roof Replacement"
]
}
}
}
}Looks up one team member of the company by ProLine phone number, email
or display name. The strategies are tried in that order and the first
match wins: proline_number is normalised to E.164 and matched against
the company's ProLine phone lines; user_email is an exact,
case-sensitive match; display_name is matched on the text before its
first comma, trimmed and ignoring case.
Administrators of the ProLine platform and limited-access users are never
returned. The answer is an array with at most one element; no match, or
an empty body, gives [].
Values are sent as strings; arrays are flattened to comma-separated text.
Response shape: wrapped (see Overview → Response shapes).
| proline_number | string A ProLine phone line assigned to the member, in any common format. |
| user_email | string The member's login email, matched exactly (case-sensitive). |
| display_name | string The member's display name. Only the text before the first comma is compared, ignoring case and surrounding spaces. |
{- "user_email": "jess.park@example-roofing.com"
}{- "isBase64Encoded": false,
- "statusCode": 200,
- "statusDescription": "200 OK",
- "headers": {
- "Set-cookie": "cookies",
- "Content-Type": "application/json"
}, - "body": [
- {
- "user_id": "u3kf2x1n0cq8vb3d5t6hm4ws2e7a9zq1",
- "display_name": "Jess Park",
- "proline_number": "+15550123456"
}
]
}