# WildPipe integration API — current supported foundation

This is an authenticated tenant integration API, not anonymous CRM access. V1 provides **summary reads, idempotent contact/lead creation, committed receipt lookup, and consent-aware SMS and customer email submission/status**, not comprehensive business-workflow writes. Signed inbound contact and lead webhooks support standard envelopes and configurable field mappings. Other inbound actions are not yet supported. The legacy automation actions remain separately available with the limitations below.

## Versioned SMS submission (v1.6)

`POST /v1/sms` requires `write:sms`, `Content-Type: application/json`, and an `Idempotency-Key` (1–128 letters/digits/underscore/period/colon/hyphen, starting with a letter or digit). The body accepts exactly `company_id`, `contact_id`, `message` (nonblank, at most 1600 characters), and `message_class` (`service` or `marketing`). Company and contact must match exactly and belong to the key's organization and permitted companies. The destination is the contact's saved primary US/Canada phone; raw destinations, sender overrides, media, schedules, templates and verification/compliance bypasses are not accepted.

Submission uses the canonical SMS workflow and its current suppression, consent, usage and sender/campaign checks. Marketing requires separate marketing permission. The API never changes consent. A durable operation is recorded before the sender is invoked. Retries with the same key and exact body bytes return that operation; changed bytes return 409. A claimed operation is never automatically submitted again, including after a crash or ambiguous error.

`GET /v1/sms/{key}` reads only that API key's operation, with current scope/company/contact authorization. Both routes return `{data: {id, state, sms_id, created_at, finished_at}}` without message text, destination or provider identifiers. HTTP 200 / `submitted` means provider submission and a saved message record, **not handset delivery**. HTTP 202 / `attempting` means the original attempt may still be running. `needs_reconciliation` means no successful submission was proven; this includes refusals after claim and uncertain provider/persistence results. Attempts still unresolved after two minutes are shown as `needs_reconciliation` but remain eligible for the original attempt's final result. Inspect the company conversation/provider records before any deliberate new send; never change the request key to retry an uncertain outcome. There is no automatic resend or reconciliation action in this version. Identities are retained indefinitely.

These semantics are separate from `/v1/receipts`, which still covers contacts/leads only. Before claim, validation errors can be corrected normally. On a 503, retry only the same key and exact body or poll its status. Structural tests are not live provider or load acceptance certification.

## Customer email submission (v1.7)

`POST /v1/emails` requires `write:emails`, `Content-Type: application/json`, and the same `Idempotency-Key` format as SMS. The body accepts exactly `company_id`, `contact_id`, `subject` (nonblank, up to 200 characters, no line breaks), and `message` (plain text, nonblank, up to 10,000 characters); the JSON body is limited to 64 KiB. The exact company-owned contact must have email permission enabled and not be blocked. The destination is its saved email, never a caller-supplied address. No HTML, CC/BCC, attachments, sender overrides, templates or scheduling are accepted.

Messages use the canonical company email sender, current suppression checks, branding, usage controls and unsubscribe links. The API never grants consent and never sends as a personal mailbox or WildPipe platform sender. The scope is supported on company-restricted keys; existing keys receive no new permission automatically.

`GET /v1/emails/{key}` returns only the issuing key’s authorized operation: `{data: {id, state, email_id, created_at, finished_at}}`. HTTP 200 / `submitted` means the provider accepted the message and its canonical sent-email record was saved, not that it reached the inbox. HTTP 202 / `attempting` or `needs_reconciliation` means successful submission has not been proven. An unresolved attempt older than two minutes is shown as needing reconciliation. No automatic resend occurs, including after interruption or failure; the original attempt may still finish.

Reuse the same key and exact request bytes to retrieve the existing result; changed bytes conflict with HTTP 409. Missing receipts do not prove an in-flight request cannot commit. Never use a new request identity to retry an uncertain send. Refusals after claim also require review. Canonical sender errors are not exposed. This adapter has structural tests, not live email-provider or full acceptance certification.

## Credentials

An active, unrestricted organization admin or owner creates an API key in Settings → API Keys. The organization must be active. Keys belong to that organization, use explicit scopes, and are shown only once. Store keys server-side; never embed them in websites or mobile bundles. Revoking a key blocks subsequent requests. A disabled/demoted/departed issuer, inactive organization, or company-restricted issuer also blocks the key.

Keys may be organization-wide (`company_ids: null`) or restricted to 1–100 selected companies. Company access is immutable: create a replacement key to change it. Restricted keys exclude operational records without a company; services/products shared across the organization remain readable, as do catalog items assigned to a selected company. This applies to list/detail reads, polling and contact search; query filters only narrow the selected scope.

For restricted keys, `POST /v1/contacts` requires a permitted company name; `POST /v1/leads` requires an existing contact belonging to a permitted company. Inbound endpoints require a fixed permitted `company_id`, which takes precedence over sender mappings. Committed receipts remain subject to company access. Restricted keys do not support legacy `/actions/*`, pest-sighting polling or referral polling. Job writes remain unavailable; `write:sms` and `write:emails` are supported through their versioned submission routes. This is not a complete workflow API.

Outbound destinations have a separate immutable company selection in Settings → Webhooks. Restricted destinations support contact, lead, job, task, quote, invoice, payment and the documented communication event families when the source provides a resolvable canonical identity. Selecting a family does not establish all-writer coverage: the event-source gaps documented below still apply. Changing a key does not change an independently configured outbound destination.

Send `X-API-Key: <your key>` over HTTPS. The configured limit is 120 requests per minute per key; 429 responses include retry guidance. The shared limiter uses an instance-local fallback if its database counter is unavailable. This is not a strict cross-instance quota during database degradation.

## Versioned read API

Base URL:

`https://api.wildpipe.com/functions/v1/zapier-api/v1`

Machine-readable specification (does not require a key):

`https://api.wildpipe.com/functions/v1/zapier-api/v1/openapi.json`

Existing project-domain integration URLs remain supported.

Each resource supports `GET /<resource>` and `GET /<resource>/<uuid>`:

| Resource | Required scope |
| --- | --- |
| 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 |

These are explicitly allowlisted summaries, not raw database rows. They exclude credentials, payment-processor references, private files, quote/invoice line-item internals, and arbitrary nested metadata. The specification lists the exact returned columns. Task status and timestamps are readable; the API does not bypass task completion or approval rules.

List parameters: `limit` (1–100, default 25) and `cursor` (the opaque `next_cursor` returned by the previous request). Supported exact-reference filters are listed below. Other filters are rejected rather than silently ignored. A detail request accepts no query parameters. Each parameter may appear only once.

```json
{"data": [], "next_cursor": null, "has_more": false}
```

Lists order by `created_at` then `id`, retaining database timestamp precision. URL-encode the cursor when sending it back. Pagination is not a point-in-time database snapshot; concurrent writes can change subsequent pages. Detail responses use `{"data": {...}}`. Errors use `{"error": "..."}` with appropriate HTTP status. Database failures return errors, never fabricated empty success lists.

## Legacy automation endpoints

Base URL is the same without `/v1`. These routes are **not** a complete v1 mutation contract:

- `GET /auth/test`
- `GET /triggers/new_contact`, `/new_job`, `/job_completed`, `/new_invoice`, `/new_payment`, `/new_lead`, `/new_pest_sighting`, `/referral_event`
- `GET /searches/find_contact?email=...` (also phone or name; literal prefix search)
- `POST /actions/create_contact`
- `POST /actions/create_lead`
- `POST /actions/create_job`
- `POST /actions/send_sms`

Legacy polling returns up to 100 recent records; use v1 reads for pagination. Search strings must not contain filter syntax or wildcard operators.

Contact creation accepts separate `first_name`/`last_name`, email, phone, company name, notes, and optional address/city/state/zip. It now stores an address through the canonical saved-address transaction rather than nonexistent contact columns. Contact creation and its subscribed outbound event commit together. New API-created contacts do **not** gain SMS/email/marketing permission merely from supplying a phone/email; do not treat API creation as consent evidence.

SMS accepts `to`, `message` (up to 1600 characters), optional `contact_id`, and `company_id`. Multi-company organizations must specify the company. When supplied, the contact must match the destination and company. Sending delegates to the canonical SMS workflow and retains consent/suppression/sender checks. Provider submission is not proof of handset delivery.

**Legacy writes do not yet provide a general idempotency-key contract. Do not blindly retry an ambiguous write or SMS response.** Legacy create-job creates a basic job record; it is not the full appointment/task/quote booking workflow. Payment charging, quote acceptance, task completion, and other protected business mutations are not offered by v1.

## Outbound webhooks

Active, unrestricted organization admins/owners configure endpoints in Settings → Webhooks. Company-restricted administrators cannot manage organization-wide endpoints. Use public HTTPS destinations on the default port; embedded credentials, private/local addresses, and redirects are refused by the outbound fetch boundary. New endpoint forms generate a signing secret; keep it configured to authenticate deliveries. Legacy unsigned endpoints are not silently broken.

“Send Test” now enqueues a real `test.ping` request. A queued response means durable acceptance, **not delivery**. The worker checks each minute and claims at most 10 deliveries per invocation. Delivery state and latest results refresh in the settings page without closing forms.

Envelope:

```json
{
  "id": "stable-event-uuid",
  "event": "contact.created",
  "created_at": "event-creation-timestamp",
  "data": {}
}
```

`X-Webhook-Delivery` equals the event ID and remains unchanged on retries and manual replay. **Receivers must deduplicate event IDs, persist receipt before acknowledging, and process idempotently.** Delivery is at least once; ordering is not guaranteed. A timeout can mean the receiver accepted the event but WildPipe did not receive acknowledgement.

### Signature verification

For configured signing secrets:

- `X-WildPipe-Signature: t=<unix-seconds>,v1=<hex-HMAC-SHA256>` signs `<unix-seconds>.<exact raw body>`.
- Verify with constant-time comparison and a suitable freshness tolerance (for example five minutes). Use raw request bytes, not parsed/re-serialized JSON. Verify freshness for each attempt; the signed attempt timestamp changes even though the event ID/body remain stable.
- Existing receivers can continue verifying `X-Webhook-Signature: sha256=<hex-HMAC-SHA256>`, which signs only the raw body. Prefer the timestamped signature for replay resistance.
- `X-Webhook-Event` identifies the event; `X-WildPipe-Attempt` identifies the attempt within the current delivery cycle.

The payload, destination URL, custom headers, and signing secret are snapshotted when enqueued. Editing an endpoint affects future events, not already queued deliveries or their manual replays. Keep previous secrets available while old deliveries drain. Custom headers cannot override event identity, signatures, transport headers, or the content type.

### Retries and recovery

2xx acknowledges delivery. 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 increase the delay. Minute-based scheduling means actual attempts can run later. Other HTTP errors exhaust immediately. Endpoint disablement cancels work when next claimed; an already in-flight request cannot be recalled.

Workers use fenced claims with two-minute leases. An interrupted attempt is retained in the attempt ledger and recovered with a new claim. Stale workers cannot overwrite the new result. Failed/exhausted deliveries can be deliberately replayed from Delivery Details; replay retains the event ID and frozen destination, resets the attempt cycle, and records the administrator in the audit log. Historical pre-queue attempts are not auto-replayed because prior receipt is unknown.

Receiver response bodies are deliberately not retained for new deliveries: they can contain credentials or unbounded data. HTTP status and bounded outcome messages are retained. Deleting an endpoint still deletes its associated delivery history; disable it instead when history must be retained.

## Transactional document events

All quote and invoice writers now capture these subscribed events in the same transaction as the source row:

- `quote.created` and `invoice.created`: record insertion.
- `quote.accepted` and `quote.declined`: a change into that status.
- `invoice.paid`: a change into paid status.

Writing the same status again emits no additional status event. An inserted document emits its created event, not a historical series of status events. Rollback discards both the source change and its queued delivery. These events describe committed record transitions, not completion of separate follow-up actions such as notifications, commissions, or payment settlement. No historical backfill runs.

Document envelopes contain stable record/company/contact/job identifiers, document number, status and totals; quote events also include version, and invoice events include quote ID and amount paid. They exclude line items, notes, signatures and processor details. Existing company restrictions and stable event-ID retry behavior apply.

## Transactional task events

Every task writer captures `task.created` on insertion and `task.completed` on a transition into Completed (status 50). Pending Approval is not completion. Existing completion validation remains required; a rejected or rolled-back completion cannot deliver an event. Inserting a historical completed task emits only its created event, not a synthetic completion history.

`task.rescheduled` means an authored timing change: appointment/all-day flags, start/end times, arrival or access windows, completion/due deadlines, or multi-day project dates. Route order, solver ETAs, assignment-only changes, and unchanged timing do not emit it. A write changing both completion status and authored timing emits both events.

Payloads contain task/company/job/contact/parent-task IDs, status ID, completion timestamp and the authored timing fields listed above. They contain no instructions, notes, signatures or raw metadata. Crew tasks are distinct records with their own events and `parent_task_id`; a task event does not imply the entire job or downstream follow-up work is complete. Each committed transition has its own stable event ID; retries preserve that ID. Creation reflects the row when inserted, not later assignment steps. Company restrictions and rollback guarantees match document events. No historical backfill runs.

## Transactional job events

Every job writer captures `job.created` on insertion. `job.updated` covers changes to the core summary fields only: company ID, contact ID, booking number, status or phase. Notes, address edits, background counters and timestamp-only writes do not emit this event. Transitions into completed or cancelled additionally emit `job.completed` or `job.cancelled`; unchanged statuses do not repeat those events. Inserts in a terminal state emit creation only, not synthetic history.

The payload contains only `id`, `company_id`, `contact_id`, `booking_number`, `status` and `phase`. Existing validation, company restrictions, atomic rollback and stable-ID delivery retries apply. Events describe the saved job record, not completion of subsequent billing, notification or other workflow steps. The legacy API no longer sends a second post-save job-created event. No history is backfilled.

## Transactional contact and lead events

All contact and lead writers now capture creation in the same transaction as the inserted row. Creation event IDs retain the record UUID, preserving the API's existing identity convention. API/inbound replay returns its original receipt without inserting a new record or event. Existing API communication-consent defaults are unchanged; receiving a webhook does not grant communication permission.

`contact.created` retains its payload fields: `id`, `first_name`, `last_name`, `email`, `phone`, `created_at`. `contact.updated` includes those fields plus `company_id` and emits only when company assignment, first/last name, email or phone changes. Notes, saved-address changes, consent preferences and timestamp-only changes are outside this event contract. Company restrictions are resolved from the saved record, including for creation.

`lead.created` contains `id`, `contact_id`, `company_id`, `source`, `status`, `created_at`. `lead.status_changed` emits on a changed stored status and additionally includes `previous_status`, `job_id`, `quote_id`. A transition into `won` (case/whitespace normalized) also emits `lead.converted`; that event means the lead was marked won, not that a job, payment or subsequent workflow necessarily completed. Inserts already marked won emit creation only. Reopening and later marking won is a new transition. Notes-only, linkage-only and unchanged-status updates do not emit these lifecycle events.

The API-only creation enqueue calls and legacy post-save lead notification were removed to avoid duplicate producers. Payloads exclude notes and raw metadata. Current company restrictions, rollback and stable-ID retry behavior apply. No historical backfill runs.

## Transactional payment events

`payment.received` is captured when a payment is inserted as `succeeded` or its stored status changes into `succeeded`, across all payment methods. Pending, failed and other non-succeeded states do not emit it; unchanged succeeded statuses, notes edits and reconciliation-only updates do not repeat it. Existing payment validation and financial processing are unchanged.

This event describes a successful **payment record**, not a new processor charge, settlement, or completed invoice allocation. Returning a reversed payment to succeeded is another status transition, not another collection of money; consumers must reconcile by `payment_id` and not add the amount to revenue for every event. `previous_status` is null on insertion and records the prior state on updates. Delivery retries retain the same event ID; separate transitions have separate IDs.

The shared payload is `id`, `payment_id` (same ID), `company_id`, `contact_id`, `quote_id`, `amount`, `base_amount`, `tip_amount`, `method`, `status`, `previous_status`, `created_at`. Amounts are the saved payment amounts, not a recomputation of processor fees; `created_at` remains record creation time, while the envelope timestamp dates the event. Notes, billing details and provider identifiers are excluded. This replaces the previous Stripe-only payload: `stripe_payment_intent_id` and `invoice_ids` are no longer emitted, and `method` is the stored value. Allocations can happen later and are deliberately not claimed in this event. Consumers relying on the old optional fields must use the documented summary instead.

The three Stripe post-save producers have been removed. Source validation, company restrictions, atomic rollback and delivery retries apply. No historical backfill or refund event is introduced, and this does not add API payment/refund mutations.

## Transactional inbound SMS events

Organization-wide endpoints subscribed to `sms.received` receive a summary when a tenant SMS record is inserted as inbound/received or transitions into that combination. Outbound and blocked messages do not emit it. Repeated callbacks, body changes and linkage-only updates while already received do not emit another event. A later transition back into received is a separate event; consumers reconcile by `sms_id`. Imports inserting received records can emit events too: this is saved-record evidence, not a guarantee of live arrival. Existing rows are not backfilled.

The payload contains only `id`, `sms_id`, `contact_id`, `job_id`, `direction`, `status`, `num_media`, `num_segments`, `created_at`. Message body, phone numbers, media URLs, provider identifiers and raw metadata are excluded. References reflect the row at capture time and can be null; later contact/job linking is not a second received event.

Recognized STOP/START/HELP-class controls are excluded using provider control metadata and exact keywords. The live SMS handler also persists its resolved control classification, including a YES interpreted as resubscription. This does not change consent handling or grant permission to send a response. SaaS Platform Inbox messages use separate storage and are not included.

Company-restricted endpoints can subscribe to SMS, call and form events. Communication company identity resolves from an unambiguous configured business phone and, when available, the matching provider account; website submissions use the website's organization-validated company. Contact-form and widget questions use their business endpoint. Missing or ambiguous identity excludes restricted delivery rather than borrowing a contact's brand. Atomic rollback and stable event-ID delivery retries apply; source capture does not establish successful HTTP delivery or downstream automation.

## Transactional call and customer-form events

`call.completed` captures a canonical visible call inserted as completed or transitioning to completed. Hidden/internal legs, non-call records, other terminal statuses and unchanged completed statuses do not emit it. Its summary contains `id`, `call_id`, `contact_id`, `job_id`, `direction`, `status`, `duration`, `created_at`, and resolved `company_id`; recordings, transcripts, phone numbers and raw metadata are excluded. Later duration/linkage updates do not emit another event. Imports can emit saved-record events; no historical backfill is performed.

`form.submitted` captures website submission inserts (`source_kind=website`, `website_config_id`) and contact-form/booking-widget question activities (`source_kind=contact_form`, `contact_id`). Both include `id`, `form_id` (the submission ID), `created_at`, and resolved `company_id`. Website conversation mirrors are excluded to avoid duplicate submission events. Form answers, messages, names, email addresses and phone numbers are excluded. Internal standalone forms and other activity types are not covered.

## Important coverage limits

The durable transport begins when an event reaches its queue. Existing producers that invoke dispatch after committing their business operation still have a source-to-queue failure window. Contact/lead, document/task/job, succeeded-payment, inbound SMS and the call/form events above are transactional. Event choices in the settings UI are subscription categories, not a guarantee that every writer currently emits every category.

Remaining work includes full canonical mutation coverage and idempotency beyond contact/lead creation, company scoping for future adapters, additional inbound actions and operational replay tooling, remaining transactional event capture, richer resource detail/subresources, operational retention/monitoring, and live end-to-end/load/concurrency acceptance testing. This foundation must not be advertised as the completed comprehensive public API.

## Idempotent contact creation (v1)

`POST /v1/contacts` requires `write:contacts` and `Idempotency-Key` (1–128 ASCII letters/digits/underscore/dot/colon/hyphen, starting with a letter/digit). Send the contact fields documented above as a JSON object, at most 32 KiB. Contact creation, saved-address materialization, subscribed outbound event, audit entry, and response receipt commit in one database transaction.

A new request returns HTTP 201 with `{"data": {"id": "..."}, "replayed": false}`. An identical retry returns HTTP 200 and the original result with `replayed: true`. Reusing the same key with different **exact body bytes** returns 409; do not reformat JSON between retries. Receipts are scoped to the issuing API key and retained indefinitely. Credential/issuer authorization is checked even on replay. A 503 may be retried using the same key and body. This guarantee does not extend to legacy actions, SMS, charges, or other resources.

## Signed inbound contact and lead webhooks

These receivers create contacts or leads, according to their immutable configured action. They cannot accept payments, complete tasks, bypass approvals, or assert communication consent.

### Configure using your integration key

All management requests use `X-API-Key`, require the matching action scope (`write:contacts` or `write:leads`), and can access only that key's own endpoints:

- `POST /v1/inbound-webhooks`: create with `name`, optional `action` (`create_contact` by default or `create_lead`), optional organization-owned `company_id`, and optional `mapping`. Returns `id` and **one-time** `signing_secret`. Maximum 50 active endpoints per key.
- `GET /v1/inbound-webhooks`: newest 100 configurations, without secrets.
- `GET /v1/inbound-webhooks/{id}`: configuration array, without secrets; unavailable or unauthorized endpoint IDs return 404.
- `POST /v1/inbound-webhooks/{id}/rotate-secret`: returns a replacement secret once; invalidates the previous secret immediately. Coordinate rotation with the sender.
- `DELETE /v1/inbound-webhooks/{id}`: revokes intake but preserves configuration and receipts. Create a new endpoint to resume.

Mapping is immutable. Create a replacement endpoint to change it. A fixed `company_id` overrides any supplied company label. This is routing confinement for the receiver, not a company-restricted parent API key.

Example configuration:

```json
{"name":"Partner contact intake","mapping":{"first_name":"customer.first","last_name":"customer.last","email":"customer.email"}}
```

Mapping keys must be supported fields for the configured action. Values are dotted object-property paths, at most eight segments and 256 characters. Arrays, expressions, transformations, prototype traversal, arbitrary destination fields, and code execution are not supported. Missing source values are omitted; mapped values must be strings or null. Contacts require at least one identity field; leads require a valid existing `contact_id`. Separate first and last names remain separate.

### Send events

Receiver URL (note: **without `/v1`**):

`https://api.wildpipe.com/functions/v1/zapier-api/inbound/contacts/{id}`

Send POST with `Content-Type: application/json`, an uncompressed body of at most 64 KiB, and:

- `X-WildPipe-Event-Id`: stable provider event identity, using the same syntax/length as an idempotency key.
- `X-WildPipe-Signature`: `t=<unix-seconds>,v1=<lowercase-hex-HMAC-SHA256>`.

Sign the UTF-8 prefix **`<timestamp>.<endpoint-uuid>.<event-id>.` followed by the exact raw body bytes**, using the one-time secret as the HMAC key. This binds the destination and event identity as well as the payload. Timestamps must be within five minutes; regenerate the signature/timestamp for retries but keep event ID and body unchanged. This inbound format intentionally differs from the outbound signature: outbound event identity is inside its signed envelope.

With no mapping, the standard body is:

```json
{"data":{"first_name":"Pat","last_name":"Example","email":"pat@example.com"}}
```

With the example mapping, send `{"customer":{"first":"Pat","last":"Example","email":"pat@example.com"}}`.

The receiver synchronously executes the canonical contact transaction. A 201 means committed; a 200 with `replayed: true` means already committed; 409 means the event ID was reused with different bytes. Invalid mappings/JSON return 422 and cause no mutation; bad/stale signatures return 401. Retry network failures/503 with the same event ID/body. Receipt and contact cannot commit separately, so a lost response does not justify creating a new event ID. Parent key revocation, issuer demotion/inactivation, organization suspension, endpoint revocation, and secret rotation are rechecked transactionally before execution/replay. Signed validation/conflict rejections retain bounded metadata for review (below), not raw bodies or an automatic replay queue.

## Idempotent lead creation (v1.2)

`POST /v1/leads` requires `write:leads`, `Content-Type: application/json`, and `Idempotency-Key` with the same syntax as contact creation. The body is at most 32 KiB and accepts only `contact_id` (required UUID), `company_id` (optional UUID or null), `source` (optional string, at most 200 characters), and `notes` (optional string or null).

The contact must belong to the authorized organization. Company resolves from the supplied company ID or the contact’s own company; an unbranded contact requires an explicit company. A branded contact cannot be linked to a different company. The lead starts as `new`; blank source defaults to `api`. This does not book work, create a new contact, change consent, or execute financial actions.

Lead creation, subscribed `lead.created` event, audit, and response receipt commit together. HTTP 201 returns `{data: {...}, replayed: false}`; identical retries return HTTP 200 with the original result and `replayed: true`; different exact bytes under the same key return 409. Payload fields are validated (422), unavailable credentials or related records return 403, and transient transaction failures return 503. Retry ambiguous responses with the SAME key and exact body bytes. Receipts are retained indefinitely. The idempotency namespace is per API key AND resource, so a contact and lead may use the same key independently. The legacy `/actions/create_lead` route is unchanged and does not inherit this guarantee. Signed inbound lead webhooks use the same canonical transaction (see v1.3 below).

## Recover a lost API response

`GET /v1/receipts/contacts/{idempotencyKey}` or `GET /v1/receipts/leads/{idempotencyKey}` requires the original API key and its matching write scope. No query parameters are accepted. A committed receipt returns `{data: <original creation result>, committed_at: <timestamp>}`. This is the creation snapshot, not the entity’s current state. It excludes raw request bodies, hashes, and inbound webhook receipts.

HTTP 404 means no committed receipt was visible at lookup time—not that an in-flight request will never commit. Retry the original POST with its original key and bytes; never create a new identity merely because lookup returned 404. Database lookup failures return 503 instead of pretending the receipt is missing. A revoked or no-longer-authorized credential cannot retrieve old receipts.

## Lead intake and rejected-event review (v1.3)

Create a lead receiver with `POST /v1/inbound-webhooks` and `{"name":"Partner leads","action":"create_lead","mapping":{"contact_id":"customer.id","source":"provider"}}`. Requires `write:leads`. Contact endpoints retain their default behavior. Actions and mappings are immutable; create a replacement endpoint to change them.

Send signed POST events to `https://api.wildpipe.com/functions/v1/zapier-api/inbound/leads/{id}`. The signature format, freshness limit, size limit, and event identity rules are identical to contact intake. A lead endpoint cannot be used on the contact URL or vice versa. Default envelopes use `{"data":{"contact_id":"<existing-contact-uuid>","source":"partner"}}`. Allowed fields are contact_id, company_id, source, and notes. The contact/company rules from POST /v1/leads still apply; a fixed endpoint company overrides the incoming company ID but never bypasses the branded-contact match.

Lead, outbound event, audit and endpoint-scoped receipt commit together. Retrying an already committed event with the same bytes returns its original result. Neither action changes consent or bypasses billing, booking, or approval workflows.

### Review rejected attempts

- `GET /v1/inbound-webhooks/{id}/rejections`: returns rejection IDs, event IDs, reasons, attempt counts, first/last-seen times, and reviewed_at.
- `POST /v1/inbound-webhooks/{id}/rejections/{rejectionId}/review`: acknowledges the entry without executing any business action. A subsequent identical rejection increments its count and clears reviewed_at.

Both routes require the same issuing API key and its matching action scope. No query parameters are supported. At most the newest 1,000 unique event-ID/body-hash combinations are retained per endpoint; reviewed entries count toward that limit and remain visible until aged out by newer records. Repeated identical rejections update one entry atomically. Body hashes stay private; raw request bodies, secrets, and raw database error messages are never retained in this history.

Only successfully signature-verified validation failures (422) and content conflicts (409) enter this ledger. Unauthenticated traffic, oversized/unreadable bodies, authorization failures, and transient infrastructure failures do not. The response includes a rejection_id when recorded; failure to persist required rejection evidence returns 503 (or 403 if authorization was withdrawn), not a pretend recorded rejection.

This is a review history, not a replay queue. Correct invalid data at the sender and resubmit; a 422 has no committed business receipt. A 409 may refer to an already successful event: investigate before changing event identity. For a timeout or 503, retain the original ID and bytes. Review does not imply successful processing or automatically resend anything.

## Exact-reference list filters (v1.4)

| Resource | Supported UUID filters |
| --- | --- |
| contacts | company_id |
| jobs | company_id, contact_id |
| tasks, quotes | company_id, contact_id, job_id |
| invoices, leads | company_id, contact_id, job_id, quote_id |
| payments | company_id, contact_id, quote_id |
| services, products, companies | none |

Example: `GET /v1/invoices?company_id=<uuid>&contact_id=<uuid>&limit=25`. Filters combine with AND and the authorized organization constraint. They match only the direct reference on each record: contact_id does not expand job-contact links, billing contacts, or historical relationships. Payment job_id filtering is not supported; payments do not carry that direct reference. Catalog company_ids arrays are not treated as company_id. Null/unlinked rows do not match a supplied UUID. Unknown or other-organization IDs return no matching rows, not a cross-tenant lookup.

Repeat the same filters when sending next_cursor. UUID case and query parameter order are normalized; limit may change between pages. Adding, removing, or changing a reference filter while reusing a cursor returns 400; start a new pagination sequence instead. Cursors issued before filter support are usable only with unfiltered lists. Cursors are navigation state, not authorization or a snapshot of changing records. Duplicate parameters, empty or invalid UUIDs, and unsupported filters return 400.

These filters narrow an already organization-authorized read. They are NOT company-restricted credentials: key issuer restrictions and organization-wide key permissions are unchanged.
