Examples
curl examples for the public API, webhook registration, and automation workflow operations.
Examples
Current V2 status (2026-09-22): In V2 the Create A Lead, List Leads, and Create A Client examples work as written, except that V2 ignores
service_interestand thestatuslist filter, and client creation requiresaddress_line1,city,state, andzip. Notes work on lead creation and on client creation and update, so the clientPATCHexample that sendsnotesworks too. Reads of estimates, jobs, and invoices work, and so do the webhook registration examples with a live key. Live-keyPOST /api/v1/estimatesdraft creation is pending production; until released, it answers405 write_not_supported. Estimate update, job, invoice, and lead update write examples remain unavailable with live keys. Automation workflow examples below require an eligible live key.
These examples use curl, but the same request shapes work in Postman, Zapier, Make, or your own backend.
Set your key first:
export CEPRO_API_KEY="ce_live_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxx"Discover and run application workflows
The V2 workflow gateway uses the full operation names and input schemas returned by its catalog. Start with the live-key catalog; it shows the operations available to the current key and member, each operation's kind (query or mutation), and its required workflow scope:
curl https://cleanestimatev2.vercel.app/api/v1/workflows \
-H "Authorization: Bearer $CEPRO_API_KEY"To read the input JSON Schema for one operation, add its full operation path as the operation query parameter. This example inspects jobs.list:
curl --get https://cleanestimatev2.vercel.app/api/v1/workflows \
-H "Authorization: Bearer $CEPRO_API_KEY" \
--data-urlencode 'operation=jobs.list'The schema detail also includes input_schema_status:
complete: the JSON Schema matches the runtime input parser.partial: use the schema as guidance; it broadens a Date, custom, or union
constraint, so the runtime parser may reject a schema-valid input.
unsupported: the operation is still listed and runtime-validated, but no
honest JSON Schema is available. Do not guess its input shape; use an independently documented contract before calling it.
The detail may include input_schema_limitations explaining broadened constraints. A no-input operation has input_schema: false with status complete; omit the input parameter or body as the operation requires.
Query inputs are JSON encoded in the input query parameter; curl --get --data-urlencode encodes it safely:
curl --get https://cleanestimatev2.vercel.app/api/v1/workflows/jobs.list \
-H "Authorization: Bearer $CEPRO_API_KEY" \
--data-urlencode 'input={"status":"scheduled","limit":25}'For larger query inputs, send the operation's raw JSON input as a POST body. Workflow POSTs require Content-Type: application/json, including no-input operations. POST queries use workflows:read and do not require Idempotency-Key:
curl https://cleanestimatev2.vercel.app/api/v1/workflows/jobs.list \
-X POST \
-H "Authorization: Bearer $CEPRO_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @job-query.jsonMutations take the JSON input described by that operation's schema as the request body and require both workflows:write and an Idempotency-Key. Use a new key for each new operation and reuse it only for the exact same input. Dispatch by the catalog's kind, not just an operation name ending in .read; some file and PDF operations can invoke a metered provider and are classified as mutations. Check the operation's permissions and effects first: catalog entries can send messages, change money, delete data, or call metered providers.
curl https://cleanestimatev2.vercel.app/api/v1/workflows/OPERATION_PATH \
-X POST \
-H "Authorization: Bearer $CEPRO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: integration-change-001" \
--data-binary @operation-input.jsonThe mutation response's meta.request_id identifies its durable receipt:
curl https://cleanestimatev2.vercel.app/api/v1/workflows/requests/REQUEST_ID \
-H "Authorization: Bearer $CEPRO_API_KEY"Receipt lookup accepts either exact workflows:read or workflows:write.
Receipts report pending, indeterminate, or completed. A pending or indeterminate mutation is never automatically run again. A same-key, same- input retry returns the recorded outcome without reclaiming the mutation; depending on that outcome, the response is the original recognized error or a 202 receipt. A changed input with the same key returns 409. After completion, an exact retry replays the saved response and includes X-Idempotent-Replayed: true. For operations that return secrets, only the first response can include the sensitive value; the stored receipt and replay return the redacted result. A recognized mutation error preserves its HTTP status and includes a receipt id and receipt_status: "indeterminate"; an unknown server failure returns a 202 indeterminate receipt. Neither outcome reclaims the operation. A receipt lookup or retry rechecks the original key, member, current permission, and location-scope access. Changed access hides a receipt with 404 or rejects a replay with 409. GET query input is limited to 64 KiB after decoding. Workflow POST bodies default to 1 MiB; an operation may have a lower limit or a reviewed higher limit up to the 4 MiB absolute cap. JSON responses are capped at 4 MiB. Inline file bytes are capped at 3,000,000 bytes to leave room for base64 and JSON overhead. Vercel's non-streamed response ceiling is 4.5 MB, so use the operation's signed upload or download flow for larger files when provided.
The gateway uses live keys and exact workflows:read or workflows:write scopes. The active key-creator's current app permissions are checked for each call, and the key must cover all locations in that member's current location scope. Legacy * and workflows:* scopes do not authorize this gateway. See Auth and Scopes for details.
Mobile and sync operations
The catalog also includes mobile availability, canvassing, estimate, inbox, job, lead, pay, time-off, timesheet, training, and workforce operations, plus the powersync.upload.apply sync action. These use fixed route mappings and preserve the original role, self-scope, permission, location, origin, consent, and rate-limit checks. In particular, mobile.jobs.schedule.options is classified as a mutation even though it uses an existing GET handler; choose GET or POST from the catalog's kind, not the underlying route verb.
Mobile GET inputs are limited to 64 KiB and other inputs to 1 MiB by default. mobile.estimates.accept allows 512 KiB, and powersync.upload.apply allows 4 MiB. Query arrays are capped at 20 strings per key; inbox channel supports repeated query parameters. The discovered schema provides the fixed route fields and query names; a PowerSync upload schema is marked partial because its runtime validator has constraints that JSON Schema cannot express. The serialized response must remain within the 4 MiB gateway cap.
File, media, and PDF operations
File operations appear in the same catalog under files.*. Follow each operation's discovered schema and kind. Some uploads use validated base64 input; other media and document uploads reserve a short-lived signed storage URL, then verify the upload in a separate complete operation. The API does not fetch caller-provided URLs. Responses return bounded content or an authorized, typed redirect. Signed upload URLs are returned only on the initial response and are redacted from durable receipts and replays; if that response is lost, create a fresh upload intent with a new idempotency key.
| File workflow | Limit and transport |
|---|---|
files.crewPhoto.upload | Up to 3,000,000 inline bytes; JPEG, PNG, WebP, or HEIC |
files.inboxAttachment.upload | Up to 3,000,000 inline bytes; supported images, PDF, or binary attachment; stored-file reads are capped at 3,000,000 bytes |
files.holidayDesignImage.upload | Up to 3,000,000 inline bytes; JPEG, PNG, or WebP |
files.companyLogo.upload | Up to 3,000,000 inline input bytes; JPEG, PNG, or WebP; normalized output is capped at 2 MiB |
files.estimatePhoto.prepareUpload, files.qualityPhoto.reserve | Up to 8 MiB; JPEG, PNG, or WebP; upload to the returned signed URL and then call complete |
files.priceBookThumbnail.prepareUpload | Up to 5 MiB; JPEG, PNG, or WebP; upload to the returned signed URL and then call complete |
files.estimateAttachment.prepareUpload, files.invoiceAttachment.prepareUpload | Up to 20 MiB; upload to the returned signed URL and then call complete |
bugReports.submit with screenshot | Up to 3,000,000 inline bytes; JPEG, PNG, or WebP data URL; the stored screenshot is normalized to WebP |
bugReports.screenshot | Returns normalized WebP, capped at 1 MiB |
The bug-report catalog also exposes bugReports.list, bugReports.get, and bugReports.update. Submission is tenant-scoped, deduplicates by requestId, and retains its existing origin and rate-limit checks. Report list, detail, triage, and screenshot operations retain the all-location web-admin manage_team requirement.
Operation-specific file limits and MIME types are enforced by the existing route handlers. Raw inline file input and output are capped at 3,000,000 decoded bytes, and the whole JSON request and response are capped at 4 MiB (or a lower per-operation limit). Oversized input or output returns HTTP 413 response_too_large without truncation. If a mutation has already had an effect, its receipt remains indeterminate; inspect the receipt before starting another operation. Voicemail recordings support validated Range requests for bounded chunks; partial responses include status: 206 and contentRange. Binary PDF and Street View results are returned only on the first response; a receipt or replay returns a redacted result. A new idempotency key invokes the provider again and may incur another charge. Estimate PDF, holiday design Street View, and mockup generation can call metered providers; the catalog marks these as mutations, so they require workflows:write and an idempotency key even when an operation name ends in .read.
Read and update existing automation workflows
These routes operate on existing workflows only. Use a live key with automations:read for both reads and automations:write for changes; the key owner also needs the permissions and organization-wide access described in Auth and Scopes. Test keys cannot call these routes.
List existing workflows, then read the selected workflow's full details:
curl https://cleanestimatev2.vercel.app/api/v1/automations \
-H "Authorization: Bearer $CEPRO_API_KEY"
curl https://cleanestimatev2.vercel.app/api/v1/automations/AUTOMATION_ID \
-H "Authorization: Bearer $CEPRO_API_KEY"The list response contains a data array of workflow summaries. The detail response contains data with the full draft and active definitions.
To edit a workflow, start from its current detail response and submit the full definition—not a partial patch of selected fields. The request body contains a positive expectedRevision from the version you reviewed and a complete definition with a name (1–120 characters) and trigger. conditions and exitConditions default to empty arrays when omitted. Include actions for a legacy linear workflow; graph drafts may omit it. The OpenAPI document at /api/v1/openapi.json describes the complete trigger schema. For graph edits, send the full graph and triggerConfig snapshots; the trigger configuration must match definition.trigger.subjectType. editorState may also be sent as a full snapshot or null. A legacy linear workflow can omit the graph snapshot, but an existing graph workflow cannot be downgraded by omitting it.
curl https://cleanestimatev2.vercel.app/api/v1/automations/AUTOMATION_ID \
-X PATCH \
-H "Authorization: Bearer $CEPRO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: automation-draft-demo-001" \
--data-binary @automation-update.jsonautomation-update.json contains the reviewed revision and full definition from the detail response, plus any full graph snapshots you are changing. A stale expectedRevision returns 409; read the latest details and review the changes before saving again. The save response includes revision; saving does not publish the draft.
Publish only the exact draft revision you intend to activate, or pause the existing workflow with a separate request:
curl https://cleanestimatev2.vercel.app/api/v1/automations/AUTOMATION_ID/publish \
-X POST \
-H "Authorization: Bearer $CEPRO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: automation-publish-demo-001" \
-d '{"draftRevisionNumber": 12}'
curl https://cleanestimatev2.vercel.app/api/v1/automations/AUTOMATION_ID/pause \
-X POST \
-H "Authorization: Bearer $CEPRO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: automation-pause-demo-001" \
-d '{}'The publish response includes revision; the pause response includes the workflow id and status: "paused".
These operations do not create or delete workflows, enroll contacts, or start a manual run. Every write needs a new Idempotency-Key for a new operation; reuse the same key only when retrying the exact same request.
Run application workflows over MCP
The V2 MCP server also exposes the application workflow catalog. Connect to POST https://cleanestimatev2.vercel.app/api/mcp using stateless Streamable HTTP and protocol version 2025-11-25. Authenticate with a live API key that has the exact workflows:read or workflows:write scope. The key creator must be active, have the current application permission for each operation, and the key must cover that member's full location scope. Test keys, legacy *, and workflows:* do not authorize these tools.
| Tool | Arguments | Purpose |
|---|---|---|
list_workflows | {} | List workflow operations available to this key and member |
get_workflow_schema | { operation: "jobs.list" } | Read an operation's name, kind, required scope, and input JSON Schema |
query_workflow | { operation, input } | Run a query using JSON input that matches its discovered schema |
mutate_workflow | { operation, input, idempotencyKey } | Run a mutation with a durable idempotency receipt |
get_workflow_request | { requestId } | Read a mutation's durable receipt |
Catalog, schema, and query tools require workflows:read; mutations and their retries require workflows:write. query_workflow sends operation input as a POST JSON request, without an idempotency key or mutation receipt. Receipt lookup accepts either exact scope. list_workflows returns workflows, get_workflow_schema returns schema, query/mutation calls return result, and receipt lookup returns request. Use the operation catalog and schema to check each action before calling it. Some operations send messages, change money, delete data, or call metered providers. MCP request and response envelopes are limited to 4 MiB, including MCP protocol overhead, so the available operation-result space may be smaller. Inline file bytes are capped at 3,000,000 bytes. Each operation's schema and runtime limits still apply. Tool errors set isError: true and include the REST status and error code in structuredContent.error.
The same catalog includes mobile and file workflows. For a file operation that returns a short-lived signed upload URL, upload to that URL directly and then call its complete operation; MCP does not fetch arbitrary URLs.
Mutation receipts can be pending, indeterminate, or completed. An unresolved mutation is never automatically executed a second time. Reuse its idempotency key only with identical input; poll the receipt if the result is still unresolved. Pending or indeterminate calls and receipt results set isError: true with instructions to check the receipt. A completed retry returns the persisted response. For an operation marked as returning secrets, the initial result may include the secret once; the receipt and retries contain only a redacted projection.
Use the existing automation tools over MCP
Connect an MCP client to POST https://cleanestimatev2.vercel.app/api/mcp using stateless Streamable HTTP and protocol version 2025-11-25. The server returns JSON responses; it does not support GET or SSE. Send JSON-RPC POST requests with Content-Type: application/json and an Accept header that includes both application/json and text/event-stream, as required by the protocol. Use the same live API key as for REST and keep it on your server. initialize returns protocol metadata without automation data; tools/list and every tool call authenticate and check current access. Send MCP-Protocol-Version: 2025-11-25 with notifications/initialized and subsequent requests.
The existing automation tools remain available alongside the application workflow tools above. Their tool list follows the key's automations:* scopes and the member's current permissions: read access exposes the first two tools, write access exposes the last three, and a key with both sees all five. Each tool call checks the current permissions again.
| Tool | Arguments | Result |
|---|---|---|
list_automations | {} | { automations: [...] } |
get_automation | { id: UUID } | { automation: ... } |
save_automation_draft | id (UUID), positive expectedRevision, full definition, optional graph, triggerConfig, and editorState snapshots, and idempotencyKey | { automation: ... }; saves a draft only |
publish_automation | id (UUID), positive draftRevisionNumber, and idempotencyKey | { automation: ... }; publishes that exact draft revision |
pause_automation | id (UUID) and idempotencyKey | { automation: ... }; pauses the workflow |
The write tools use the same revision checks and idempotency protection as the REST routes. A save needs the full current definition; graph saves use the matching trigger configuration. Draft saving can preserve incomplete graph actions, but publishing rejects invalid or incomplete graph actions. A stale draft returns 409 stale_revision; fetch the latest workflow and review it before saving again. Each write tool requires an idempotencyKey from 1 to 255 characters. Use a new key for a new operation and reuse it only for an exact retry. Missing or ineligible credentials return 401 or 403.
Successful calls return structured content and JSON text. Failed tool calls return isError: true with structuredContent.error containing status, code, message, and details. These tools can inspect or change existing workflows; they cannot create or delete workflows, enroll contacts, or start a manual run.
Create A Lead
curl https://app.cleanestimate.pro/api/v1/leads \
-X POST \
-H "Authorization: Bearer $CEPRO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: lead-demo-001" \
-d '{
"customer_name": "Taylor Morgan",
"customer_email": "taylor@example.com",
"customer_phone": "+15551234567",
"service_address": "123 Main St",
"city": "Charlotte",
"state": "NC",
"zip": "28202",
"service_interest": ["house-wash"],
"notes": "Requested a Saturday estimate"
}'List Leads
curl "https://app.cleanestimate.pro/api/v1/leads?limit=25&sort=created_at&order=desc&status=new" \
-H "Authorization: Bearer $CEPRO_API_KEY"Create A Client
curl https://app.cleanestimate.pro/api/v1/clients \
-X POST \
-H "Authorization: Bearer $CEPRO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: client-demo-001" \
-d '{
"name": "Taylor Morgan",
"email": "taylor@example.com",
"phone": "+15551234567",
"address_line1": "123 Main St",
"city": "Charlotte",
"state": "NC",
"zip": "28202"
}'Write Notes To Clients, Leads, And Jobs
The V1 public API supports note fields on clients, leads, and jobs. You can send notes when you create the record or patch notes onto an existing record later.
Add Notes To A Client
curl https://app.cleanestimate.pro/api/v1/clients \
-X POST \
-H "Authorization: Bearer $CEPRO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: client-notes-demo-001" \
-d '{
"name": "Taylor Morgan",
"email": "taylor@example.com",
"phone": "+15551234567",
"address_line1": "123 Main St",
"city": "Charlotte",
"state": "NC",
"zip": "28202",
"notes": "Prefers text first. Side gate code is 4421.",
"status": "active"
}'V2 requires the four address fields on every client create (each client owns a service property); V1 accepted the request without them.
curl https://app.cleanestimate.pro/api/v1/clients/CLIENT_ID \
-X PATCH \
-H "Authorization: Bearer $CEPRO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: client-notes-update-001" \
-d '{
"notes": "Requested Friday afternoon appointments only."
}'Add Notes To A Lead
curl https://app.cleanestimate.pro/api/v1/leads \
-X POST \
-H "Authorization: Bearer $CEPRO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: lead-notes-demo-001" \
-d '{
"customer_name": "Taylor Morgan",
"customer_email": "taylor@example.com",
"customer_phone": "+15551234567",
"service_address": "123 Main St",
"city": "Charlotte",
"state": "NC",
"zip": "28202",
"notes": "Requested Saturday estimate. Interested in house wash and driveway."
}'In V2, source is optional and must match the name of one of your workspace's active lead sources (for example "source": "Website"); the example leaves it out so it runs in any workspace.
curl https://app.cleanestimate.pro/api/v1/leads/LEAD_ID \
-X PATCH \
-H "Authorization: Bearer $CEPRO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: lead-notes-update-001" \
-d '{
"notes": "Left voicemail. Follow up tomorrow morning."
}'Create An Empty Draft Estimate
Pending production: This new live-key operation is not available on production yet. After release, the API key needs estimates:write, and the key's creator must be active and have access to the requested location.
curl https://app.cleanestimate.pro/api/v1/estimates \
-X POST \
-H "Authorization: Bearer $CEPRO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: estimate-demo-001" \
-d '{
"location_id": "LOCATION_UUID",
"client_id": "CLIENT_UUID",
"vertical": "residential"
}'A lead can be used in place of client_id; the lead supplies its customer, and its property is used unless property_id is supplied. If both client_id and lead_id are provided, they must refer to the same customer. An explicit property_id must belong to that customer and location. The only supported verticals are residential, generic, commercial, and holiday_lights (default: residential).
The endpoint creates an empty draft. It does not accept prices, totals, line items, or a requested status, and it does not send or accept the estimate. The response is a record projection, not a priced quote or customer link:
{
"data": {
"id": "ESTIMATE_UUID",
"location_id": "LOCATION_UUID",
"client_id": "CLIENT_UUID",
"property_id": null,
"lead_id": null,
"vertical": "residential",
"status": "draft",
"created_at": "2026-09-22T12:00:00.000Z",
"updated_at": "2026-09-22T12:00:00.000Z"
}
}The office can finish, price, and send the draft in Clean Estimate. Test API keys retain the existing isolated sandbox behavior and do not create live estimates.
The first successful create returns 201. Retrying the same request with the same idempotency key replays that 201 response; reusing the key with a different request body returns 409 idempotency_conflict. Use a new key for a distinct draft.
Photo-Assisted Pricing
POST /api/v1/estimates creates an empty draft only. It does not analyze photos, calculate a price, attach a customer-facing view_url, or send a quote. Use the supported in-app Photo-to-Price workflow where available; do not treat this draft-creation endpoint as a photo-quote or outreach integration. Broader photo-to-price API and integration parity remains tracked separately.
Update An Estimate
curl https://app.cleanestimate.pro/api/v1/estimates/ESTIMATE_ID \
-X PATCH \
-H "Authorization: Bearer $CEPRO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: estimate-update-demo-001" \
-d '{
"customer_phone": "+15559876543",
"assigned_rep_id": "00000000-0000-0000-0000-000000000123",
"status": "sent",
"expires_at": "2026-04-19T00:00:00.000Z"
}'PATCH /api/v1/estimates/:id is intentionally narrow. Use it for customer/contact metadata, address/property details, assignment, draft/sent status, source, and expires_at. If the service selection or pricing inputs change, create a fresh estimate instead of trying to reprice through this endpoint.
Register A Webhook Destination
curl https://app.cleanestimate.pro/api/v1/webhooks \
-X POST \
-H "Authorization: Bearer $CEPRO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: webhook-demo-001" \
-d '{
"name": "Ops Sync",
"url": "https://example.com/cepro/webhooks",
"events": ["lead.created", "estimate.created", "job.completed"]
}'The create response includes the webhook secret once. Store it immediately so you can verify signatures later.
ce_test_... keys cannot call /api/v1/webhooks in v1. Use a live key when you are ready to register real outbound destinations.
Create A Job
curl https://app.cleanestimate.pro/api/v1/jobs \
-X POST \
-H "Authorization: Bearer $CEPRO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: job-demo-001" \
-d '{
"client_id": "00000000-0000-0000-0000-000000000001",
"service_type": "house_wash",
"scheduled_date": "2026-03-20",
"scheduled_time_start": "09:00:00",
"scheduled_time_end": "11:00:00",
"customer_name": "Taylor Morgan",
"service_address": "123 Main St",
"service_city": "Charlotte",
"service_state": "NC",
"service_zip": "28202"
}'curl https://app.cleanestimate.pro/api/v1/jobs/JOB_ID \
-X PATCH \
-H "Authorization: Bearer $CEPRO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: job-notes-update-001" \
-d '{
"notes": "Customer requested a 30-minute arrival call.",
"access_notes": "Use side gate. Code 4421.",
"special_instructions": "Park on the street, not in the driveway."
}'List Invoices
curl "https://app.cleanestimate.pro/api/v1/invoices?limit=25&sort=created_at&order=desc&status=sent" \
-H "Authorization: Bearer $CEPRO_API_KEY"V1 webhook routes are live-key-only. Use a ce_live_... key for /api/v1/webhooks.
Pull The OpenAPI Spec
curl https://app.cleanestimate.pro/api/v1/openapi.jsonUse the OpenAPI file for:
- Swagger UI
- Postman import
- SDK generation
- Contract validation in CI
Related articles
Was this article helpful?
Still need help? Contact support