Webhooks & the Leads API

Five events cover the life of a lead — the enquiry, the pipeline, the booking and the money. Each arrives as one signed POST carrying what changed; everything else is one authenticated GET away.

Last updated: 13 August 2026

Turning it on

In the admin, go to Settings → Integrations — webhook (under Advanced). Paste the URL your receiver listens on, tick the events you want, and save. A signing secret is generated the first time a URL is saved and does not change when you save again — integrations pin it.

Tick only what you handle

An endpoint that already existed keeps receiving lead.created and lead.accepted and nothing else. That is deliberate: plenty of receivers were written when those were the only two events and treat any POST as a new lead. New endpoints start subscribed to all five.

Return a 2xx as soon as you have stored the body, then do the real work. Anything else is treated as a failure and retried.

The five events

The name arrives in the X-PriceMyDay-Event header as well as the body, so you can route before parsing.

EventFires whenchange
lead.createdA customer submits the quote wizard. Adds wizard, wizardVersion, createdAt.
lead.acceptedA customer accepts through their quote link. Adds acceptedAt.
lead.status_changedThe lead moves in the pipeline, by hand or by acceptance.from, to
lead.payment_receivedMoney recorded against the quote — typed by staff, or settled online (which also carries change.instalment). paymentId, amount, method,
reference, note, receivedAt
lead.payment_removedA recorded payment is withdrawn. same as above

Accepting fires twice for a subscriber to both: lead.accepted and a lead.status_changed to booked. Key on lead.accepted for “they said yes”; use the status event for pipeline mirroring.

A booking taken by phone still counts. Staff who type a lead in and move it to booked never touch the accept link, so lead.status_changed is the only event — and it carries a real booking.reference.

Envelope

Every event has the same shape. Write one parser, switch on event, and read change for what is specific to it.

FieldWhat it is
eventOne of the five names above.
occurredAtUTC, ISO-8601. When we built the payload.
quoteIdOur id for the lead — the one to store, and the one the API takes.
quoteNumberThe human number on the customer's PDF, in your own numbering format.
customername, email, phone.
statusPipeline status after the change: new, contacted, quoted, booked, lost.
moneytotal, tax, discount, discountCode, deposit, paid, balance.
bookingreference, date, time, durationMinutes, endTime, acceptedAt. Null reference until there is a booking.
documentsquoteNumber, invoiceNumber, bookingReference.
linksAbsolute URLs: the lead in the API and its three PDFs. confirmationPdf is null before acceptance.
changeOnly on events that have one.
Two fields say the same thing twice

Top-level total, deposit and currency repeat what is inside money. They are kept because receivers written years ago read them by path. money is canonical — read that in new code.

Balance is always after the change

money.paid and money.balance already include the payment the event announces, and already exclude the one it withdraws. You never apply change.amount yourself.

Sample payload

A real lead.payment_received, captured from a test receiver:

{
  "event": "lead.payment_received",
  "occurredAt": "2026-08-13T13:48:26.1203031Z",
  "quoteId": "019ffb61-9aef-70de-9610-e5d6b4ddb766",
  "quoteNumber": "Q-000007",
  "customer": { "name": "…", "email": "…", "phone": "+9198765…" },
  "total": 115000, "deposit": 34500, "currency": "INR",
  "status": "booked",
  "money": {
    "currency": "INR", "total": 115000, "tax": 17542,
    "discount": 0, "discountCode": null, "deposit": 34500,
    "paid": 34500, "balance": 80500
  },
  "booking": {
    "reference": "BK-20260813-000007", "date": "2026-12-20",
    "time": "18:30", "durationMinutes": 240, "endTime": "22:30",
    "acceptedAt": "2026-08-13T13:48:25.9874228Z"
  },
  "documents": {
    "quoteNumber": "Q-000007", "invoiceNumber": "Q-000007",
    "bookingReference": "BK-20260813-000007"
  },
  "links": {
    "lead": "https://pricemyday.com/api/admin/leads/019ffb61-…",
    "quotePdf": "…/quote.pdf",
    "invoicePdf": "…/invoice.pdf",
    "confirmationPdf": "…/confirmation.pdf"
  },
  "change": {
    "paymentId": "019ffb61-9c7c-7289-947a-52e723f6cf7b",
    "amount": 34500, "method": "upi",
    "reference": "428913776501", "note": "Booking advance",
    "receivedAt": "2026-08-13T13:48:26.1082895Z"
  }
}

Verifying the signature

Compute HMAC-SHA256 of the raw request body with your signing secret, hex-encode it lowercase, and compare against the header. Parse the JSON only after that passes — re-serialising the parsed object changes the bytes.

X-PriceMyDay-Event:     lead.payment_received
X-PriceMyDay-Delivery:  019ffb4d-967c-7784-b167-0bb5bb493817
X-PriceMyDay-Signature: sha256=fef7d642de1df765e55b5a09d4826796946dfc8…

Node (Express)

app.post('/hooks/pricemyday',
  express.raw({ type: 'application/json' }), (req, res) => {

  const sent = req.get('X-PriceMyDay-Signature') ?? '';
  const mine = 'sha256=' + crypto.createHmac('sha256', SECRET)
                         .update(req.body).digest('hex');

  if (sent.length !== mine.length ||
      !crypto.timingSafeEqual(Buffer.from(sent), Buffer.from(mine)))
    return res.sendStatus(401);

  const evt = JSON.parse(req.body);
  res.sendStatus(200);      // ack first…
  queue.push(evt);          // …work after
});

Python (Flask)

@app.route("/hooks/pricemyday", methods=["POST"])
def hook():
    mine = "sha256=" + hmac.new(SECRET.encode(), request.get_data(),
                        hashlib.sha256).hexdigest()
    if not hmac.compare_digest(
    mine, request.headers.get("X-PriceMyDay-Signature", "")):
return "", 401

    handle.delay(request.get_json())
    return "", 200

Delivery & retries

Events are written in the same database transaction as the change that caused them, then drained every 15 seconds. Nothing is announced that did not happen. Any 2xx is success; the request is abandoned at 10 seconds.

AttemptWaitsElapsed
1within ~15 s of the change
230 s30 s
31 min1.5 min
42 min3.5 min
54 min7.5 min
68 min15.5 min
716 min31.5 min
832 min63.5 min — then abandoned

Fetching the detail

The webhook carries what changed. For line items, the contact's wizard answers, notes and the full payment history, call the API with the quoteId you were given. Create a key under Settings → API keys and send it as X-Api-Key. Keys carry Staff access: wizards and leads, read and write; not settings, billing or team.

curl https://pricemyday.com/api/admin/leads/019ffb60-1ac9-7daf-a997-4fbc05a01c29 \
     -H "X-Api-Key: qk_…"
EndpointReturns
GET /api/admin/leadsPaged list. ?status=, ?wizardId=, ?page=, ?pageSize= (max 100).
GET /api/admin/leads/{id}The full lead: contact and their answers, line items, money, booking, payments, paymentSchedule, notes, document links.
PATCH /api/admin/leads/{id}/statusMove it in the pipeline. Body {"status":"booked"}.
PATCH /api/admin/leads/{id}/assignAssign to a team member.
POST /api/admin/leads/{id}/notesAdd a note.
GET /api/admin/leads/{id}/quote.pdfThe customer's documents, re-rendered live. Sent no-store — never cache or re-host them.
GET /api/admin/leads/{id}/invoice.pdf
GET /api/admin/leads/{id}/confirmation.pdf
GET /api/admin/leads/export.csvEverything, as a spreadsheet.

The lead detail response uses the same money and booking blocks as the webhook, so one set of types covers both.

Known limits

Contact

Integration questions: [email protected].