Connect Crisphive to Pipedream
By the end of this guide a Pipedream workflow can book and confirm a field job in Crisphive in one step, look up a caller by phone, and start automatically — with a verified signature — when a job is completed or a customer is created.
Pipedream runs workflows: a trigger starts them and steps do the work, either ready-made actions or short Node.js code. Crisphive is a field operations platform that schedules your technicians with constraint-based scheduling on a deterministic solver.
Three words you will meet below, in plain language: an API key is a long password that lets a program act for your business in Crisphive. A webhook is Crisphive calling a web address when something happens, so your workflow hears about it instantly. An HTTP request is a program calling a web address to read or change something — here, Pipedream calling Crisphive.
This guide uses Pipedream’s built-in HTTP / Webhook trigger, HTTP request and Node.js steps, calling the Crisphive public API directly.
What you can do
| Kind | Pipedream step | Crisphive API |
|---|---|---|
| Action | Book and confirm a job — HTTP request or Node.js | POST /v1/job-requests/book-and-confirm: creates the job, sizes it from the job type’s default duration and confirms it at your chosen time, with Crisphive picking the technician. |
| Search | Find a customer by phone — HTTP request | GET /v1/customers?phone=+16135550177: exact match on the number. |
| Action | Create a customer — HTTP request | POST /v1/customers |
| Action | Get a job — HTTP request | GET /v1/job-requests/{id}: status, schedule, technician, customer, address. |
| Trigger | HTTP / Webhook trigger + a Node.js signature check | Crisphive calls your workflow on events you subscribe to with POST /v1/webhooks (job completed, job confirmed, customer created, …). |
Before you start
- A Crisphive account with Developer access — by default only the business Owner has it. It is needed to create API keys, including one with the trigger scopes.
- An existing Pipedream account that can create workflows (see the note above about new sign-ups).
- Every job type you will book has a default duration (Crisphive → Settings → Job Types). The built-in “General” type already has one (60 minutes) and is used when you do not choose a type.
- About 20 minutes for the booking action, 15 more for the trigger. No programming experience is needed: every piece of code below is copy-and-paste.
Step 1 — Create a Crisphive API key
Start with a test key (it begins with chsk_test_). It works only on your business’s Rehearsal copy — the sandbox, with test data — so nothing you do reaches a real customer or technician.
In the left sidebar of the Crisphive dashboard, click Rehearsal in the Rehearsal | Live switch, then click Switch. Rehearsal is Crisphive’s sandbox: isolated test data, and no real customer or technician is ever texted or emailed. A key created while you are in Rehearsal starts with chsk_test_ and only ever touches the test data.
Go to Settings → Developer → API Keys (crisphive.com/app/settings/developer/api-keys) and click Create new API key. Creating keys needs Developer access, which by default only the Owner has.
Key type: Secret. Never Publishable — a publishable chpk_ key is only for the booking widget on your website and is refused everywhere else. Name: Pipedream.
In Expires in (days) keep 30 days for a test key. Keys always expire: 30 days by default, anything from 1 to 365 days chosen when you create the key. The Owner and Administrators are emailed 7 days before a key expires.
Under Scopes choose Restricted and select customers_view, customers_manage, job_view, job_create and job_manage. If you will use a trigger, also select developer_view and developer_manage_api_keys (subscribing and unsubscribing a webhook needs them), plus team_view if you want technician events.
Click Create API key, then Copy, then Done. The full key is shown only once — paste it into your password manager. If you lose it, create a new key and revoke the old one.
Step 1 of 6In Settings → Developer → API Keys, click Create new API key. Keep Key type on Secret — never Publishable.
All steps
- In Settings → Developer → API Keys, click Create new API key. Keep Key type on Secret — never Publishable.
- Type the name Pipedream so you recognise the key later.
- Keep Expires in (days) on 30 days for a test key. You can choose up to 365 days.
- Under Scopes choose Restricted and select customers_view, customers_manage, job_view, job_create, job_manage.
- Click Create API key.
- Click Copy. The key starts with chsk_test_ and is never shown again. Then click Done.
Step 2 — Store the key in Pipedream
Keep the key out of your workflow steps by saving it as a Pipedream environment variable — a named secret every step can read as process.env.FIELD_OPS_API_KEY, or as {{process.env.FIELD_OPS_API_KEY}} in a form field.
In Pipedream, open Settings → Environment Variables for the whole workspace, or open your project and click Variables in its left navigation for that project only. Click New Variable.
Set Key to FIELD_OPS_API_KEY and Value to your chsk_test_… key. Leave Secret on (the default) so the value stays hidden, and save.
Step 1 of 3Name the variable FIELD_OPS_API_KEY.
All steps
- Name the variable FIELD_OPS_API_KEY.
- Paste your chsk_test_… key as the value. It is hidden once saved.
- Click Save.
Step 3 — Book and confirm a job
Add a step to your workflow after its trigger (a form submission, a new spreadsheet row, a phone-system event…). You can build the request with Pipedream’s HTTP request form (option A) or paste a Node.js step (option B). Both send the same call.
Option A — HTTP request form.
Click + after the trigger, choose HTTP / Webhook, then the action that sends an HTTP request.
Method POST, URL https://api.crisphive.com/v1/job-requests/book-and-confirm.
Authorization = Bearer {{process.env.FIELD_OPS_API_KEY}} (the word Bearer, one space, the key). Content-Type = application/json. Idempotency-Key = pd-{{steps.trigger.context.id}}-book — see why.
Choose a raw JSON body and paste the template below, replacing the values with data from your trigger (for example {{steps.trigger.event.body.phone}}).
Click Test. A green result with "error_code": 0 and "confirmed": true means the job is booked.
Step 1 of 5Set the method to POST and the URL to the book-and-confirm endpoint.
All steps
- Set the method to POST and the URL to the book-and-confirm endpoint.
- Add the Authorization header: Bearer, a space, then the environment variable.
- Add Content-Type: application/json and an Idempotency-Key built from the trigger’s event ID.
- Paste the JSON body: the customer, the service address, the start time on the business’s clock and a description.
- Click Test. The response shows error_code 0, confirmed: true and a short_code.
The body, ready to copy:
{
"customer": {
"full_name": "Alex Martin",
"phone": "+16135550177",
"email": "alex.martin@example.com"
},
"address": {
"line": "145 Laurier Avenue West",
"city": "Ottawa",
"state": "ON",
"postal_code": "K1P 5J3",
"country": "CA"
},
"scheduled_at": "2026-10-06T10:00:00",
"description": "Furnace stopped heating"
}| Field | Required | Meaning |
|---|---|---|
customer | Yes (or customer_id) | full_name plus a phone (E.164, with + and country code) or an email. Crisphive matches an existing customer by phone or email and creates one only if there is no match — never a duplicate. Add "sms_opt_in": true only if the customer explicitly agreed to texts. |
address | Yes for a new caller | Where the work happens. line plus a city or postal_code, so Crisphive can find a technician who covers that area. |
scheduled_at | Yes | Start time on the business’s own clock, with no timezone offset: 2026-10-06T10:00:00. Never add Z or -04:00. |
customer_id | Instead of customer | An existing customer’s ID. Crisphive then uses the address stored on the customer; one with no stored address is refused with JOB_REQUEST_ADDRESS_REQUIRED. Sending customer + address is safer. |
job_type_id | Optional | Empty = your default job type (“General”). Get IDs from GET /v1/job-types. |
job_duration_minutes | Optional | Empty = the job type’s default duration. |
description | Optional | The problem in the customer’s words (up to 2,000 characters). |
A successful answer:
{
"error_code": 0,
"message": "Success",
"data": {
"job_id": "7b2f0c1e-3a44-4d1b-9a5e-0e6f1c2d3b4a",
"short_code": "REQ-CA-YCNFQUUV",
"customer_id": "c4d1a9e2-8f3b-4c6d-a1e2-5f7b9c0d1e2f",
"confirmed": true,
"scheduled_at": "2026-10-06T14:00:00Z",
"assigned_technician_id": "1e9d7c5b-3a2f-4e1d-8c7b-6a5f4e3d2c1b"
}
}error_code0means success; anything else is a stable error code (see Troubleshooting).scheduled_atin the answer is UTC — 14:00 UTC is 10:00 in Ottawa in October.- If
data.confirmedisfalse, the job was saved but no technician can take it at that time.data.refusal.error_codesays why (for exampleJOB_REQUEST_NO_TECHNICIAN_AVAILABLE), and the job waits in the coordinator’s queue. Notify a person; do not send the booking again.
Option B — Node.js step. Add a Node.js code step and paste:
export default defineComponent({ async run({ steps, $ }) { // Replace these with data from your trigger, e.g. steps.trigger.event.body.phone const booking = { customer: { full_name: "Alex Martin", phone: "+16135550177", email: "alex.martin@example.com", }, address: { line: "145 Laurier Avenue West", city: "Ottawa", state: "ON", postal_code: "K1P 5J3", country: "CA", }, scheduled_at: "2026-10-06T10:00:00", // business local time, no offset description: "Furnace stopped heating", }; const res = await fetch("https://api.crisphive.com/v1/job-requests/book-and-confirm", { method: "POST", headers: { Authorization: "Bearer " + process.env.FIELD_OPS_API_KEY, "Content-Type": "application/json", // Same event => same key => a retry never books twice. "Idempotency-Key": "pd-" + steps.trigger.context.id + "-book", }, body: JSON.stringify(booking), }); const envelope = await res.json(); if (envelope.error_code !== 0) { throw new Error("Crisphive " + envelope.error_code + ": " + envelope.message); } const job = envelope.data; $.export("$summary", job.confirmed ? "Booked " + job.short_code : "Saved " + job.short_code + " — not yet scheduled (" + job.refusal?.error_code + ")"); return job; }, });
Step 4 — Find a customer by phone
Method GET, URL https://api.crisphive.com/v1/customers, header Authorization = Bearer {{process.env.FIELD_OPS_API_KEY}}.
Parameter phone = the number in E.164, for example +16135550177 (Pipedream encodes the + for you when you use the parameters list).
data.customers is the list of matches. One entry: you found the caller (use data.customers[0].id). Empty: a new caller. Each entry has id, full_name, tier, status, request_count and last_request_at.
Step 1 of 3Set Method to GET and URL to https://api.crisphive.com/v1/customers.
All steps
- Set Method to GET and URL to https://api.crisphive.com/v1/customers.
- Add the query parameter phone with the caller’s number in E.164.
- Add the Authorization header and click Test.
6135550177) is refused with 400 PHONE_INVALID, never answered with an empty list — so an empty list really means “new caller”. To create a customer without booking, POST https://api.crisphive.com/v1/customers with {"full_name": "…", "phone": "+1…"} and an Idempotency-Key; the answer is {"customer_id": "…"}.Step 5 — Start a workflow on a Crisphive event
Here you create a workflow with an HTTP address, tell Crisphive to call that address on the events you choose, and add a step that checks every call really comes from Crisphive.
New workflow → trigger HTTP / Webhook → New Requests. Keep the default response (HTTP 200 straight away). Configure it to pass the raw request body to the workflow if offered — the signature is computed over the exact bytes Crisphive sent. Copy the trigger’s URL (it looks like https://eo….m.pipedream.net).
Call POST https://api.crisphive.com/v1/webhooks once with that URL and the events you want — from a terminal with the command below, or from a one-off Node.js step. Ask for "expires_in_days": 365: the default is 30 days, after which the subscription stops.
The answer’s data.status must be active. Crisphive sent a test ping to your URL while creating it; Pipedream answered 200, so the subscription is live. Copy data.id (to remove it later) and data.secret (whsec_…, shown once).
Add a second environment variable, FIELD_OPS_WEBHOOK_SECRET, with the whsec_… value.
Right after the trigger, add a Node.js step with the verification code below. It stops the workflow for the ping and for anything not signed by Crisphive, and returns the event for the next steps.
Deploy the workflow. Every matching Crisphive event now runs it within seconds.
Subscribe from a terminal (replace the URL and key):
curl -X POST "https://api.crisphive.com/v1/webhooks" \ -H "Authorization: Bearer chsk_test_4eC8xQ9mZ2pL7Ka0rT" \ -H "Idempotency-Key: pipedream-subscribe-2026-10-04" \ -H "Content-Type: application/json" \ -d '{ "url": "https://eo1234567890abc.m.pipedream.net", "event_types": ["job_request.completed", "customer.created"], "description": "Pipedream workflow", "expires_in_days": 365 }'
Or from a Node.js step you run once with Test (then delete the step):
export default defineComponent({ async run({ $ }) { const res = await fetch("https://api.crisphive.com/v1/webhooks", { method: "POST", headers: { Authorization: "Bearer " + process.env.FIELD_OPS_API_KEY, "Content-Type": "application/json", "Idempotency-Key": "pipedream-subscribe-" + Date.now(), }, body: JSON.stringify({ url: "https://eo1234567890abc.m.pipedream.net", // your trigger URL event_types: ["job_request.completed", "customer.created"], description: "Pipedream workflow", expires_in_days: 365, }), }); const envelope = await res.json(); if (envelope.error_code !== 0) { throw new Error("Crisphive " + envelope.error_code + ": " + envelope.message); } // Copy id and secret now: the secret is never shown again. return { id: envelope.data.id, status: envelope.data.status, secret: envelope.data.secret }; }, });
The verification step (paste as the first Node.js step after the HTTP trigger):
import { createHmac, timingSafeEqual } from "node:crypto"; // Crisphive-Signature: t=<unix seconds>,v1=<hex>[,v1=<hex>] // v1 = HMAC-SHA256 of "<t>.<raw body>" with the whsec_ secret. // Accept if ANY v1 matches (two are sent for 24 h after a secret rotation) // and t is within 5 minutes of now. function verify(rawBody, header, secret, toleranceSec = 300) { if (!header || !secret || rawBody == null) return false; let t = ""; const sigs = []; for (const part of String(header).split(",")) { const i = part.indexOf("="); if (i < 0) continue; const k = part.slice(0, i).trim(); const v = part.slice(i + 1).trim(); if (k === "t") t = v; else if (k === "v1") sigs.push(v); } const ts = Number(t); const now = Math.floor(Date.now() / 1000); if (!t || !Number.isFinite(ts) || Math.abs(now - ts) > toleranceSec || sigs.length === 0) return false; const body = Buffer.isBuffer(rawBody) ? rawBody : Buffer.from(String(rawBody), "utf8"); const want = createHmac("sha256", secret).update(t + ".").update(body).digest(); return sigs.some((s) => { const got = Buffer.from(s, "hex"); return got.length === want.length && timingSafeEqual(got, want); }); } export default defineComponent({ async run({ steps, $ }) { const event = steps.trigger.event; const raw = event.bodyRaw ?? (typeof event.body === "string" ? event.body : undefined); const body = typeof event.body === "string" ? JSON.parse(event.body) : event.body; // The ping arrives while the subscription is being created, before you // have the secret. It is never a real event: stop here. if (body?.type === "ping") return $.flow.exit("Crisphive ping — nothing to do"); if (raw === undefined) { throw new Error("No raw body: set the HTTP trigger to pass the raw request body"); } const headers = event.headers ?? {}; const signature = Object.entries(headers) .find(([name]) => name.toLowerCase() === "crisphive-signature")?.[1]; if (!verify(raw, signature, process.env.FIELD_OPS_WEBHOOK_SECRET)) { return $.flow.exit("Invalid Crisphive-Signature — ignored"); } // { id, type, created_at, business_id, environment, data: { object } } return { event_id: body.id, type: body.type, object: body.data?.object ?? {} }; }, });
- Job events are thin. A
job_request.*event carries the job’sid,short_codeand status. Add a GET request tohttps://api.crisphive.com/v1/job-requests/{{steps.verify.$return_value.object.id}}(replaceverifywith your step’s name) to read the customer, address, schedule and technician. - Repeats. Crisphive delivers at least once. Use
event_idto skip an event you already handled. - Events you can subscribe to:
GET /v1/webhooks/event-typeslists them —job_request.created,.confirmed,.assigned,.rescheduled,.completed,.archived,.status_changed,.priority_changed,customer.created/.updated/.deleted,technician.created/.updated/.deleted. - Scopes. Subscribing (
POST /v1/webhooks) and unsubscribing (DELETE) needdeveloper_manage_api_keys; listing event types needsdeveloper_view. Each event family also needs read access:customer.*needscustomers_view,job_request.*needsjob_view,technician.*needsteam_view; otherwise403 WEBHOOK_EVENT_NOT_PERMITTED. - Turning it off. When you no longer need the trigger, call
DELETE https://api.crisphive.com/v1/webhooks/{id}with the same key, or delete the endpoint in Crisphive → Settings → Developer → Webhooks. A business can have at most 25 active webhooks per environment. - Lifetime. With
expires_in_days: 365the secret lasts a year; Crisphive emails the Owner 7 days before. Subscribe again (and store the new secret) before then. A subscription also stops when the key that created it is revoked.
GET /v1/job-requests/{id} using your API key and act on what Crisphive returns, never on the event body.AI agents: Pipedream Connect, MCP and the lasting path
If what you want is an AI assistant that books and manages jobs, you do not need a Pipedream workflow at all. Crisphive runs its own MCP server at https://api.crisphive.com/mcp — MCP (Model Context Protocol) is the standard AI assistants use to discover and call tools. Point any MCP client at it with the header Authorization: Bearer chsk_…, or sign in to Crisphive with OAuth and approve — the connection then acts as the member who approved, with that member’s role. Smaller tool sets: /mcp/voice (caller lookup and one-call booking), /mcp/crm, /mcp/dispatch. See MCP server.
Worked workflows
- Web form → confirmed job. HTTP trigger receiving your form → the Node.js booking step from Step 3 with
steps.trigger.event.bodyfields → ifconfirmedis true, send the customer an email withshort_code; otherwise post “needs a time” to your team chat. - Caller lookup. Phone-system trigger → Step 4 lookup → if one customer, book with their phone and the address they give; if none, book with full name, phone and address (Crisphive creates the customer).
- Job completed → review request. HTTP trigger subscribed to
job_request.completed→ verification step → GET the job → emailcustomer.emailwith your review link. - New customer → spreadsheet. HTTP trigger subscribed to
customer.created→ verification step → appendobject.full_name,object.phone,object.emailandevent_idto a sheet.
Retries never book twice
The Idempotency-Key header is a label meaning “this is the same request as before”. Built from steps.trigger.context.id (the ID Pipedream gives each incoming event), it stays the same if Pipedream retries the step for that event, and Crisphive replays the first answer instead of booking again. A new event gets a new ID and is a new booking. Never build the key from the current time in a booking step — every retry would then book a second job. Sending the same key with a different body is refused with 422 IDEMPOTENCY_KEY_REUSE.
Test it
Use these fictional details. The 555-01xx range and example.com are reserved for fiction, and Crisphive never texts or emails them.
Run the Step 4 request with phone=+16135550177. Success: error_code 0 and customers is empty (a new caller) or holds Alex Martin.
Run the Step 3 request with the body shown (Alex Martin, +16135550177, alex.martin@example.com, 145 Laurier Avenue West, Ottawa, ON K1P 5J3, CA, “Furnace stopped heating”) and a weekday-morning scheduled_at inside your working hours. Success: confirmed: true and a short_code.
Run the same step again with the same Idempotency-Key. Success: the identical answer — still one job in Crisphive.
Switch the dashboard to Rehearsal. The job appears on the board with status Confirmed (opened, its window is titled Booking Confirmed), a technician assigned and the Laurier Avenue address.
Complete that Rehearsal job on the dashboard. Success: within seconds your receiving workflow runs, and the verification step returns type: "job_request.completed".
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
401 API_KEY_INVALID | Key mistyped or revoked, “Bearer ” missing, or a publishable chpk_ key. | Header must read Bearer chsk_test_… exactly; check FIELD_OPS_API_KEY. |
401 API_KEY_EXPIRED | The key reached the end of its lifetime. | Create a new key and update the environment variable. |
403 API_KEY_SCOPE_INSUFFICIENT | The key lacks a permission; data.required_scope names it. On POST or DELETE /v1/webhooks it is developer_manage_api_keys; on the event-types list, developer_view. | Create a key that includes it — for triggers, add developer_view and developer_manage_api_keys to the booking scopes. |
403 WEBHOOK_EVENT_NOT_PERMITTED | The key cannot read that event type; the message names the permission. | Create a key that also has it — team_view for technician events. |
400 PHONE_INVALID | No country code, e.g. 6135550177. | Send E.164: +16135550177. |
400 JOB_REQUEST_ADDRESS_REQUIRED | No address with a city or postal code — often customer_id for a customer without a stored address. | Send customer + address with city or postal_code. |
400 JOB_REQUEST_INVALID_INPUT about the time (data.means_locally) | scheduled_at has a timezone offset, or is in the past. | Send the business’s local clock with no offset, in the future. |
400 JOB_REQUEST_QUOTE_INVALID, job_type_has_no_default_duration | The job type has no default duration. | Set one in Settings → Job Types, send job_duration_minutes, or omit job_type_id. |
confirmed: false with a refusal | Saved, but no technician can take it then. | Do not resend. Pick it up in the dashboard queue. |
422 IDEMPOTENCY_KEY_REUSE | Same key, different body — including a corrected request resent after an error, because Crisphive stores the first answer (even a refusal) under its key. | Reuse a key only to resend the exact same request after a timeout or no answer. If you change anything in the request, use a new key (for example add -2 to it). |
409 IDEMPOTENCY_IN_PROGRESS | The first attempt is still running. | Wait a second and retry with the same key. |
429 TOO_MANY_REQUESTS | More than 240 requests per minute on one key. | Wait Retry-After seconds. |
Webhook status is pending_verification | The ping did not get a 2xx (URL wrong, workflow deleted or paused). | Delete it (DELETE /v1/webhooks/{id}), fix the URL, subscribe again. |
409 WEBHOOK_LIMIT_REACHED | 25 active webhooks already exist in this environment. | Delete stale ones under Settings → Developer → Webhooks. |
| Every event exits with “Invalid Crisphive-Signature” | Wrong secret stored, or the body is not the raw bytes. | Store the whsec_ value from the subscribe answer; make the trigger pass the raw body. |
| Events stopped arriving | The secret expired, the key that subscribed was revoked, or 5 deliveries in a row failed (the endpoint is disabled). | Check the endpoint under Settings → Developer → Webhooks; subscribe again with a valid key. |
Go live
In the left sidebar, click Live in the Rehearsal | Live switch and confirm with Switch, then repeat Step 1: the new key starts with chsk_live_. Choose a lifetime up to 365 days and put the expiry date in your calendar; the Owner and Administrators are also emailed 7 days before.
Set FIELD_OPS_API_KEY to the live key. Every step reading it switches at once.
Sandbox subscriptions only receive sandbox events. Repeat the subscribe call with the live key, store the new whsec_ secret in FIELD_OPS_WEBHOOK_SECRET, and delete the sandbox subscription.
Live jobs land on your real board, real technicians are assigned, and customers receive confirmations by email (by text only with sms_opt_in: true).
Keys cannot be extended: create the replacement, update the variable, check a run, then click Revoke access on the old key in Settings → Developer → API Keys. Revoking stops every call and disables the webhooks that key created.
Related guides and reference
- Automation platforms overview — what every automation integration shares
- n8n — the Crisphive node for self-hosted n8n, plus n8n AI Agents
- Zapier — book jobs from any Zap; instant job and customer triggers
- Make — the same booking and triggers in Make scenarios
- MCP server reference — every tool, the tool profiles and client setup
- Webhooks reference — events, signatures and retries
- Book and confirm a job — the API call behind every booking step
FAQ
Do I need a Crisphive app on Pipedream?
Is Pipedream shutting down?
Will a Pipedream retry book the same job twice?
Idempotency-Key built from steps.trigger.context.id. A retry for the same event replays the first answer.Why did my job not confirm?
confirmed: false means the job is saved but no technician could take it then — outside working hours, nobody covering that address or skill, or everyone busy. refusal.error_code says which. It waits in the coordinator queue; do not resend.How do I know a webhook really came from Crisphive?
Crisphive-Signature header: an HMAC-SHA256 of the raw body with your whsec_ secret, as in the verification step above. Reject anything that does not match or is older than 5 minutes.What is the difference between a chsk_test_ and a chsk_live_ key?
chsk_test_ uses an isolated sandbox copy of your business — no real customer is contacted. chsk_live_ uses your real jobs and customers.