WRAPTORHelp & Docs
The Wraptor handbook

API & Webhooks — connect Wraptor to Zapier and your own tools

Who it's for

Owners (and the tech-comfortable manager) who want Wraptor talking to the rest of their stack: Zapier automations, a custom dashboard, a spreadsheet that pulls live numbers, or lead sources that push straight into the Lead Inbox. Included with Wraptor Shop and Wraptor Fleet.

Where it fits

Outside the job workflow itself — this is the integration layer around it. Events fire from the moments that matter to the shop: a lead arrives, a job moves a stage, a customer approves a quote, a payment lands.

Overview

Two halves:

  • The REST API (https://wraptor.app/api/v1) lets outside software read your customers, jobs, quotes, invoices, leads and purchase orders — and create customers and leads. Every request authenticates with an API key you mint in Settings.
  • Outbound webhooks push events to a URL you control the moment they happen, signed so your receiver can prove they came from Wraptor. This is what a Zapier trigger rides on.

The machine-readable spec lives at /api/v1/openapi.json.

Screens & navigation

Settings → API & Webhooks (under Integrations & Billing, or straight to /settings?section=developer). One panel, two cards:

  • API Keys — every active key on one row: its name, its display prefix (wrk_live_a1b…), either "used 3d ago" or "never used", and a Revoke… link at the end.
  • Webhook Endpoints — every registered URL with the events under it ("All events" when you picked none), a green dot when it is on and a grey one when it is off, and Test · Disable/Enable · Delete…. Underneath, Recent Deliveries lists the last 20 real deliveries: the event name, the attempt number once one has been retried, the error and HTTP code on a failure, and how long ago it ran.

Capabilities

API keys

  • Create key — name it after what will hold it ("Zapier", "reporting sheet"). The full key appears once, in a copy box headed "Your new API key — copy it now, it won't be shown again." Only a hash and the first twelve characters are stored; Wraptor cannot show it again.
  • Revoke… — a two-step inline confirm. It takes effect immediately.
  • Up to 10 active keys per shop. Each key is shop-wide: full access to everything below, with no per-resource scopes.
  • The last-used time is refreshed at most every five minutes, so a busy key can read a few minutes behind.

REST API

  • List and get by id: customers, jobs, quotes, invoices, purchase-orders. Leads are list-only.
  • Write: POST /customers and POST /leads. Both answer 201 with the new record, or 200 with the record already on file when the body is a duplicate — so a client retry or a CRM re-sync never makes a second copy. A customer is a duplicate on the same email or phone, or on the same name when neither is sent. Email, name and lead-contact matches ignore case and surrounding spaces but are otherwise exact (a_b@x.com never matches axb@x.com). A lead is a duplicate when its contact, vehicle, service and message match one saved in the last 24 hours (at any age when it is sent with "imported": true).
  • In a write body, an empty string or null counts as not sent. A rejected body answers 400 with a message that names the field (contact: …).
  • Idempotency-Key. Every POST and PATCH takes an optional Idempotency-Key header: any unique string up to 255 characters (a UUID works). Send the same key with the same path and body again within 24 hours and you get the first answer back, with Idempotent-Replayed: true, and nothing runs a second time — no second record, bell or webhook. Keys belong to your shop, so two shops never collide.
    • Same key, different body or path: 422, code idempotency_key_reused. Use a new key for a new request.
    • A retry that arrives while the first request is still running: 409, code idempotency_in_progress, with Retry-After: 1. Retry and you get the stored answer.
    • Only 2xx answers are stored. After a 400 or other error you can fix the body and retry with the same key.
    • Key order and spacing in the JSON body do not matter. After 24 hours the same key runs as a new request; stored answers are deleted after 7 days.
    • Without the header nothing changes: the duplicate checks above still apply. Use a key whenever your client or agent retries on timeouts.
  • Lists paginate by cursor: ?limit= (25 by default, 100 at most) and ?cursor= taken from the previous page's next_cursor, which is null on the last page. ?updated_since= (an ISO 8601 timestamp) narrows a list to what changed since your last poll — the Zapier polling filter. Leads never change once captured, so theirs filters on the created time.
  • A successful read returns { "data": … }, a list adds next_cursor, and anything that fails returns { "error": { "code": …, "message": … } } with a matching HTTP status.
  • GET /me verifies a key and names the shop (Zapier's auth test).
  • 120 requests a minute per key; over the limit returns 429 with a Retry-After header.
  • GET /jobs leaves out the seeded sample job. A job's customer_id is null when it was imported without a customer.
  • Quotes and invoices carry the second tax line, when there is one, as tax_amount_2 and tax_label_2.
  • An API-created lead lands in the Lead Inbox and rings the bell ("New API lead: …"), which opens that lead's card. Its source is api, and it files under the Imported filter on Leads → Leads.
  • Send "sms_consent": true on POST /leads only when the person agreed to get texts from you. Replies from the Lead Inbox go to a phone contact by text only when it is on; a phone number alone never counts. It defaults to false. Re-sending a saved lead with true turns it on; leaving it out never turns it off. Leads read back with sms_consent.

Webhook events

job.created, job.stage_changed, quote.approved, quote.declined, proof.approved, proof.changes_requested, document.signed, invoice.paid, invoice.deposit_paid, lead.created, booking.created, customer.created, inspection.completed, purchase_order.ordered, purchase_order.received, call.completed, call.missed, payroll.finalized.

Every delivery uses the same envelope — event, entity_type, entity_id, created_at, and a data object holding that event's own fields:

{
  "event": "job.stage_changed",
  "entity_type": "job",
  "entity_id": "…",
  "created_at": "2026-09-06T15:04:05.000Z",
  "data": { "job_number": "…", "title": "…", "from": "…", "to": "…" }
}
  • job.created fires when a job is created in the app — the New Job form, Rex on your say-so, Convert to job or Create quote on a Leads card (email leads included), a customer sending the public /request-quote form, or an online booking request. A job minted by a quote approval does not repeat it — that arrives as quote.approved, whose payload carries the new job_id — and neither do jobs added by a bulk import.
  • quote.approved and quote.declined fire when the customer answers on their own quote page. Recording the yes yourself with the Mark approved sheet does not fire them.
  • invoice.paid and invoice.deposit_paid fire for card and bank payments through Stripe and for payments you record by hand (cash, Zelle, check). Either way the payload carries invoice_number, amount, method, invoice_total, fully_paid and job_id. Once your shop offers monthly payments, method can also be AFFIRM, KLARNA or FINANCING (Affirm or Klarna, when Stripe didn't say which).
  • inspection.completed fires when a check-in/check-out vehicle inspection is submitted (either lane — installer tablet or front desk). The payload carries the report id, job id, kind, each checklist item's grade, photo and video counts, and the signer name/time — no file URLs.
  • purchase_order.ordered fires when a PO is marked ordered or emailed to the vendor; purchase_order.received fires when it becomes fully received (a short-close counts, flagged short_closed: true). Payloads carry the PO number, status, vendor id/name, the ordered/expected/received timestamps, line count and cent totals — line detail comes from GET /purchase-orders/{id}.
  • call.completed / call.missed fire when a call to the shop's tracking number ends (answered vs. not), so they only ever arrive once call tracking is switched on for your shop. Payloads carry the caller and tracking numbers, the matched customer/lead ids (null if unrecognized), and — for completed calls — the duration in seconds. See calls.md.
  • payroll.finalized fires when a pay period is finalized on Money → Payroll. The payload carries the period start/end, line count, total hours, and base/commission/total cent amounts — deliberately no per-person lines (wages stay inside the app; export the CSV or PDF for detail).

Endpoints

  • Add endpoint — paste the URL and tap the events you want. Pick none and the endpoint takes all of them ("No events selected — the endpoint receives all events."). The signing secret (whsec_…) is shown once, right after you add it. Up to 10 endpoints per shop.
  • Verify — each POST carries wraptor-id, wraptor-timestamp, and wraptor-signature: v1,<base64 HMAC-SHA256> over id.timestamp.body, keyed with your secret's base64 part (everything after whsec_) — the svix scheme, so any svix client library verifies it by mapping the header names. The user agent is Wraptor-Webhooks/1.0.
  • Retries — a failed delivery is due again after 2 minutes, then 8, then every 15, for up to 6 attempts (about 55 minutes end to end). The retry sweep runs every half hour, so a re-send can land later than its due time. An endpoint is switched off only when its last 3 deliveries all ran out of attempts and nothing has gone through for 24 hours. The row then reads "auto-disabled after repeated failures — fix the URL and re-enable", and the shop gets a bell and push, "A webhook was turned off", that opens Settings → API & Webhooks. Re-enabling starts a fresh count.
  • Test — fires a test.ping at one endpoint and answers with a Test delivered (HTTP 200) toast, or Test failed and the reason. A test ping is one attempt only: it is never retried and never counts toward switching an endpoint off. Test pings stay out of Recent Deliveries, and the button is greyed out while an endpoint is disabled.
  • Disable / Enable / Delete… — disabling stops deliveries and keeps the endpoint and its secret listed; re-enabling clears the auto-disable stamp; deleting removes it for good.
  • Manage endpoints from the API too: GET and POST /api/v1/webhooks, then GET, PATCH and DELETE /api/v1/webhooks/{id}. Create returns the secret once; delete unsubscribes. That is the REST-hook shape Zapier expects.

Step-by-step tasks

  1. Connect a spreadsheet or dashboard

    1. Settings → API & Webhooks → type a name → Create key.
    2. Copy the key from the one-time box, then click I've saved it.
    3. Call GET https://wraptor.app/api/v1/jobs with header Authorization: Bearer wrk_live_….
    4. Follow next_cursor until it is null; poll with updated_since after.
  2. Get notified the moment a lead arrives

    1. Add endpoint with your receiver URL, subscribed to lead.created.
    2. Store the one-time secret; verify wraptor-signature on each POST.
    3. Click Test to confirm your receiver answers 2xx.
  3. Push leads in from an outside form

    1. POST /api/v1/leads with { "name": "...", "contact": "...", "vehicle": "...", "service": "...", "timeline": "...", "message": "..." } — only name and contact are required. Add "sms_consent": true when your form asked for permission to text, and an Idempotency-Key header (one per submission) so a retry never doubles the lead.
    2. The lead appears under Leads → Leads and rings the bell. Several API leads inside 10 minutes fold into one "N new API leads" bell, which opens the Imported filter rather than one lead's card. Imported leads (next step) never ring and don't add to that count.
    3. Loading contacts you already have (a CRM sync or backfill)? Add "imported": true to the body. The lead is saved without a bell or push, and re-sending it later returns the saved one instead of a copy.

Settings & permissions

  • API keys and webhooks come with Wraptor Shop and Wraptor Fleet (and the grandfathered Wraptor Pro (original)). On Wraptor Starter the panel reads "API access is included with Wraptor Shop." with an Upgrade to Wraptor Shop → link instead of the two cards.
  • The 7-day card-backed trial does not open it — API access needs a live paid subscription. A failed payment keeps working through the grace period; once the subscription actually ends, existing keys stop authenticating (403, code plan_required) while your endpoints stay registered.
  • Opening Settings needs the settings permission, and this panel itself runs on the billing permission — reading the key list, creating a key and managing endpoints all require it. Owners and Managers have both.

Tips & common pitfalls

  • Copy secrets when shown. Keys and signing secrets appear exactly once. Lose one → revoke/delete and create a fresh one; nothing else re-reveals it.
  • Answer webhooks fast with a 2xx. Do your real work after responding — a slow receiver hits the 10s timeout and looks like a failure.
  • Don't redirect. Wraptor never follows a 3xx, so a redirect counts as a failed delivery. Register the final URL.
  • Use wraptor-id for idempotency. Retries reuse the same id; process each id once.
  • Send an Idempotency-Key on writes. Make one key per record you mean to create and reuse it on every retry of that request. A new key on each retry defeats it.
  • Localhost and private-network URLs are rejected, including IPv6 forms like [::1], [fd00::1] or [::ffff:127.0.0.1]. Receivers must be publicly reachable, over http or https.
  • Deleting an endpoint stops its deliveries immediately; disabling keeps it listed so you can re-enable without re-sharing a secret.
  • Marketing — the Lead Inbox, where API-created leads land.
  • Jobs — the stages behind job.stage_changed.
  • Invoicing & Billing — the payments behind invoice.paid.
  • Plans — what each tier includes.