Getting started
WildPipe’s tenant integration API connects external systems to the same company, customer, job, and financial records used by the operating system. The current version provides summary reads, idempotent contact and lead creation, committed receipt lookup, and signed inbound contact/lead webhooks. It is not a general-purpose database or full business-workflow write API.
An active, unrestricted organization admin or owner creates a key in Settings → API Keys and selects explicit scopes and company access. Copy the key when shown and store it only on your server. Send it in the X-API-Key header over HTTPS. Never put a key in a browser bundle, mobile app, public repository, or URL.
Base URL: https://api.wildpipe.com/functions/v1/zapier-api/v1
The live OpenAPI 3.0 specification requires no key to download and describes the current endpoints and returned fields. Import it into your preferred API client. Requests containing real credentials or records must run in your own trusted environment.
e.g. server-side JavaScript:
const base = 'https://api.wildpipe.com/functions/v1/zapier-api/v1';
const response = await fetch(base + '/contacts?limit=25', {
headers: { 'X-API-Key': process.env.WILDPIPE_API_KEY }
});
if (!response.ok) throw new Error('WildPipe request failed: ' + response.status);
const { data, next_cursor, has_more } = await response.json();
Use a separate key for each integration. Revocation prevents subsequent requests. Access also depends on the issuer’s continued eligibility and the organization remaining active.
Resources & scopes
Each resource supports GET /{resource} and GET /{resource}/{uuid}. A read scope is required for each resource:
- contacts — read:contacts
- jobs — read:jobs
- tasks — read:tasks
- quotes — read:quotes
- invoices — read:invoices
- payments — read:payments
- leads — read:leads
- services — read:services
- products — read:products
- companies — read:companies
Responses are allowlisted summaries, not complete record exports. Consult OpenAPI for exact fields. Private files, processor references, credentials, and quote/invoice line-item internals are not included.
Company access: keys can cover the organization or 1–100 selected companies. Company restrictions are immutable; create a replacement key to change them. Restricted keys omit company-less operational records but can read shared catalog items and items assigned to a selected company. Query filters can narrow access, never expand it. Restricted contact creation requires a permitted company name; lead creation requires a contact in a permitted company. Restricted inbound endpoints require a fixed permitted company ID.
Outbound webhook destinations have their own independent company selection. Changing a key does not change a webhook destination.
Pagination & filters
Lists accept limit (1–100, default 25) and cursor (the previous response’s opaque next_cursor). Results are ordered by created_at, then id. Preserve and URL-encode the cursor; do not decode or construct it yourself.
{"data": [], "next_cursor": null, "has_more": false}
Detail responses use { "data": { ... } } and accept no query parameters. Pagination is not a point-in-time snapshot: concurrent changes can affect later pages.
Supported exact UUID filters:
- contacts: company_id
- jobs: company_id, contact_id
- tasks and quotes: company_id, contact_id, job_id
- invoices and leads: company_id, contact_id, job_id, quote_id
- payments: company_id, contact_id, quote_id
- services, products, companies: none
Filters combine with AND and match direct references only; they do not expand related contacts or job relationships. Repeat the same filters on every page. Changing filters requires starting a new cursor sequence. Duplicate, unsupported, empty, or invalid parameters return 400. Payment job_id filtering is not supported.
GET /invoices?company_id=<uuid>&contact_id=<uuid>&limit=25
Send customer email
POST /emails requires write:emails, application/json, and an Idempotency-Key. Supply exactly company_id, contact_id, subject (1–200 characters, no line breaks), and message (plain text, 1–10,000 characters). The JSON body limit is 64 KiB. Email goes only to the matching company contact’s saved address through the company sender; current email permission, suppression, branding and unsubscribe controls apply. No recipient/sender overrides, HTML, CC/BCC, attachments or scheduling.
GET /emails/{key} returns this API key’s receipt: id, state, email_id, created_at and finished_at. HTTP 200 / submitted proves provider acceptance plus a saved email record, not inbox delivery. HTTP 202 / attempting or needs_reconciliation means success is unproven; attempts unresolved after two minutes are shown as needing reconciliation. The original attempt may still finish.
The same request identity and exact bytes return the existing operation; changed bytes return 409. Claims are never reissued after an error or interruption. Never use a new key to resend an uncertain outcome. Company-restricted keys are supported, but existing keys gain no permission automatically. This does not expose payment/refund operations or certify live provider delivery.
Create contacts & leads
POST /contacts requires write:contacts. Accepted fields: first_name, last_name, email, phone, company (name), address, city, state, zip, and notes. Use separate first and last names and supply at least one identity field. Supplying a phone or email does not grant communication consent.
POST /leads requires write:leads and an existing contact_id. Optional fields: company_id, source (up to 200 characters), and notes. Company defaults to the contact’s company; an unbranded contact requires an explicit company. A branded contact cannot be assigned to a different company. The lead starts as new; blank source defaults to api. This does not book an appointment.
Both requests require Content-Type: application/json, a body no larger than 32 KiB, and Idempotency-Key: 1–128 letters, digits, underscores, dots, colons, or hyphens, starting with a letter or digit.
const body = JSON.stringify({ first_name: 'Pat', last_name: 'Example',
email: 'pat@example.com', company: 'Your configured company name' });
const response = await fetch(base + '/contacts', {
method: 'POST',
headers: {
'X-API-Key': process.env.WILDPIPE_API_KEY,
'Content-Type': 'application/json',
'Idempotency-Key': 'partner-contact-123'
},
body
});
A new committed request returns 201 with data and replayed: false. An identical retry returns 200 with the original result and replayed: true. Reusing the key with different exact body bytes returns 409. Persist the serialized body before sending; do not reformat JSON between retries. The identity is per key and resource. Receipts are retained indefinitely.
For network failures or 503, retry with the same key and bytes. Contact/lead creation and their subscribed outbound event commit together.
To recover a lost response, use GET /receipts/contacts/{idempotencyKey} or GET /receipts/leads/{idempotencyKey}, with the original API key and matching write scope. The response includes the original data and committed_at. A 404 means no committed receipt was visible at lookup time; an in-flight request could still commit. Retry the original POST rather than inventing a new identity.
Outbound webhooks
Configure a public HTTPS destination in Settings → Webhooks as an eligible organization admin or owner. Select event subscriptions, company access, and a signing secret. Use the default HTTPS port and a direct destination without redirects. The settings page provides Send Test, delivery details, and deliberate replay of failed/exhausted deliveries.
Send Test enqueues test.ping. Queued means accepted for delivery, not delivered. The worker checks each minute; delivery is at least once and order is not guaranteed.
{
"id": "stable-event-uuid",
"event": "contact.created",
"created_at": "event-creation-timestamp",
"data": {}
}
X-Webhook-Delivery equals the event ID and stays the same across retries and manual replay. X-Webhook-Event names the event. X-WildPipe-Attempt identifies the attempt in the current delivery cycle. Deduplicate by event ID, persist receipt before acknowledging with 2xx, and process your own side effects idempotently.
Payload, URL, custom headers, and secret are fixed when the event is queued. Endpoint edits apply to future events, not queued deliveries or their replays. Keep prior secrets available while older deliveries drain. Disabling an endpoint cancels pending work when next claimed; it cannot recall an in-flight request. Disable rather than delete when you need to retain delivery history.
Verify outbound signatures
The preferred X-WildPipe-Signature header is t=<unix-seconds>,v1=<hex-HMAC-SHA256>. Sign the timestamp, a period, and the exact raw request body bytes. Check freshness (for example, five minutes) and compare digests in constant time before parsing JSON or processing the event. The timestamp changes per attempt; the event ID and body do not.
Node.js CommonJS verification helper (pass the raw request Buffer, before JSON middleware):
const { createHmac, timingSafeEqual } = require('node:crypto');
function verifyWildPipe(rawBody, signatureHeader, secret) {
if (!Buffer.isBuffer(rawBody) || !secret || typeof signatureHeader !== 'string') return false;
const match = /^t=(\d+),v1=([a-f0-9]{64})$/i.exec(signatureHeader);
if (!match) return false;
const timestamp = Number(match[1]);
if (!Number.isSafeInteger(timestamp) ||
Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
const expected = createHmac('sha256', secret)
.update(match[1] + '.').update(rawBody).digest();
const received = Buffer.from(match[2], 'hex');
return received.length === expected.length && timingSafeEqual(received, expected);
}
Reject invalid signatures. Do not recreate the signed bytes with JSON.stringify. After verification, parse the envelope and apply durable event-ID deduplication. Freshness alone is not deduplication.
The compatibility header X-Webhook-Signature: sha256=<hex> signs only the raw body. Prefer the timestamped signature for new integrations. Configure a signing secret before accepting production traffic; legacy unsigned endpoints may still exist.
Events, retries & coverage
Current transactional coverage:
- contact.created and lead.created for canonical API and signed inbound creation.
- quote.created and invoice.created on document insertion.
- quote.accepted, quote.declined, and invoice.paid on a change into that status.
- task.created on insertion; task.completed when a task transitions into Completed, not Pending Approval.
- task.rescheduled on authored timing changes (appointment/all-day flags, start/end, arrival/access windows, due/completion deadlines, or project dates). Assignment-only changes, route order, and solver ETAs do not emit this event.
An unchanged status emits no new transition event. Historical imports emit creation, not a reconstructed transition history. Task events include parent-task identity for crew records; completing one task does not prove the whole job or subsequent work is complete. Document/task payloads carry identifiers and summary state rather than instructions, signatures, or line-item details.
The event choices shown in settings are subscription categories, not a guarantee of all-writer coverage. Other producers have narrower coverage; verify the detailed delivery contract before depending on a category. Do not assume every product action has an event or API write endpoint.
Retries: 2xx acknowledges. Network/unknown outcomes, 408, 425, 429, and 5xx retry up to eight total attempts per cycle. Backoff starts at 30 seconds and triples, bounded to one day; bounded Retry-After can extend it. Minute scheduling may delay attempts. Other HTTP errors exhaust immediately. A timeout can occur after your receiver has already accepted an event.
Manual replay retains the original event ID and frozen destination, and starts a new attempt cycle. Never rely on a replay being a new business event. Receiver response bodies are not retained; inspect HTTP status and bounded outcome details in settings.
Signed inbound webhooks
Inbound webhooks create contacts or leads; they do not charge payments, complete tasks, accept quotes, or grant communication consent. Manage endpoints with X-API-Key and the corresponding write:contacts or write:leads scope. Each key manages only its own endpoints.
- POST /inbound-webhooks — create with name, action (create_contact by default or create_lead), optional company_id and mapping. Returns id and a one-time signing_secret. Up to 50 active endpoints per key.
- GET /inbound-webhooks — newest 100 configurations, without secrets.
- GET /inbound-webhooks/{id} — configuration array, without secrets.
- POST /inbound-webhooks/{id}/rotate-secret — replacement secret shown once; the previous secret stops working immediately.
- DELETE /inbound-webhooks/{id} — permanently revoke intake, preserving receipts.
Actions and mappings are immutable; create a replacement endpoint to change them. Fixed company_id overrides incoming company values. Restricted keys must configure a permitted company.
{"name":"Partner contact intake","action":"create_contact","mapping":{"first_name":"customer.first","last_name":"customer.last","email":"customer.email"}}
Mappings map allowed destination fields to dotted object-property paths (up to eight segments/256 characters). Values must be strings or null. Arrays, expressions, transformations, and arbitrary fields are not supported. Missing source values are omitted.
Send events to the following URLs, without /v1:
https://api.wildpipe.com/functions/v1/zapier-api/inbound/contacts/{id}
https://api.wildpipe.com/functions/v1/zapier-api/inbound/leads/{id}
POST uncompressed application/json, at most 64 KiB. Set X-WildPipe-Event-Id to a stable sender event identity (same syntax as Idempotency-Key) and X-WildPipe-Signature to t=<unix-seconds>,v1=<lowercase-hex-HMAC-SHA256>.
Inbound signing differs from outbound: sign the UTF-8 prefix <timestamp>.<endpoint-uuid>.<event-id>., followed by exact raw body bytes. Use the endpoint signing secret. The timestamp must be within five minutes. Regenerate the signature and timestamp for retries, but retain the same event ID and body.
Without mappings, use {"data":{...contact or lead fields...}}. With the example mapping, send:
{"customer":{"first":"Pat","last":"Example","email":"pat@example.com"}}
Lead intake requires an existing contact_id and accepts company_id, source, and notes. The endpoint action must match the receiver URL. 201 means committed, 200 with replayed: true means already committed, and 409 means the event ID conflicts with different bytes. Retry network failures/503 with the original identity and bytes; fix invalid 422 payloads at the sender.
Rejected-event review: GET /inbound-webhooks/{id}/rejections returns retained reasons, counts, timestamps, and review state. POST /inbound-webhooks/{id}/rejections/{rejectionId}/review acknowledges an entry but never executes or resends it. Only signed validation/content-conflict rejections appear. This history holds the newest 1,000 unique rejections, without raw bodies or secrets. Investigate a 409 before changing event identity; it may refer to a successful event.
Errors, limits & compatibility
Errors use {"error":"..."}. Handle status codes, not exact error-message strings:
- 400 — invalid query parameters or cursor; correct the request.
- 401 — unavailable credential or invalid/stale inbound signature.
- 403 — insufficient scope or no longer permitted access.
- 404 — resource or committed receipt not visible to this credential.
- 409 — identity reused with different bytes; investigate rather than blindly retrying.
- 415 / 422 — unsupported media type or invalid input; correct the payload.
- 429 — rate limited; honor retry guidance.
- 500 / 503 — request could not complete; retry reads with backoff and writes only using their documented idempotency identity.
The configured API limit is 120 requests per minute per key. Use bounded exponential backoff with jitter and respect Retry-After. Do not assume a timeout means nothing happened.
Legacy endpoints live at the base URL without /v1: /auth/test, polling /triggers/, /searches/find_contact, and /actions/create_contact, create_lead, create_job, send_sms. Polling is capped at 100 recent records. They are not the v1 mutation contract and do not provide general idempotency. Restricted keys cannot use legacy /actions/, pest-sighting polling, or referral polling. Legacy create_job creates a basic record, not a complete booked appointment. Never blindly retry ambiguous legacy writes or SMS submissions.
V1 does not provide payment charging, task completion, quote acceptance, general update/delete operations, or comprehensive workflow writes. No public sandbox or SDK is documented here; arrange testing with the organization and use deliberate test records. Follow the changelog and re-check the live specification when maintaining an integration.
Production checklist & support
Before going live:
- Select minimum scopes and company access; keep credentials in a server-side secret store.
- Validate pagination, direct-reference filters, non-2xx responses, and key revocation.
- Persist write identities and exact serialized bytes before sending.
- Verify raw-body signatures and timestamp freshness before webhook processing.
- Deduplicate events durably and make downstream side effects idempotent.
- Test duplicate delivery, out-of-order events, timeout recovery, and secret rotation.
- Confirm coverage for each event family you rely on; do not equate a task event with a whole-job outcome.
- Monitor your receiver and review failed deliveries. Check Uptime & System Status for reported incidents.
For integration questions, contact WildPipe or email staff@wildpipe.com. Include the endpoint, HTTP status, timestamp, and a non-sensitive request/event ID. Never send API keys, signing secrets, payment details, or customer payloads. In-app Help is available from the Help button at the bottom left.
