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
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.
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 name arrives in the X-PriceMyDay-Event header as well as the body,
so you can route before parsing.
| Event | Fires when | change |
|---|---|---|
| lead.created | A customer submits the quote wizard. Adds
wizard, wizardVersion, createdAt. | — |
| lead.accepted | A customer accepts through their quote link.
Adds acceptedAt. | — |
| lead.status_changed | The lead moves in the pipeline, by hand or by acceptance. | from, to |
| lead.payment_received | Money recorded against the quote —
typed by staff, or settled online (which also carries
change.instalment). |
paymentId, amount, method, reference, note, receivedAt |
| lead.payment_removed | A 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.
Every event has the same shape. Write one parser, switch on event, and
read change for what is specific to it.
| Field | What it is |
|---|---|
| event | One of the five names above. |
| occurredAt | UTC, ISO-8601. When we built the payload. |
| quoteId | Our id for the lead — the one to store, and the one the API takes. |
| quoteNumber | The human number on the customer's PDF, in your own numbering format. |
| customer | name, email, phone. |
| status | Pipeline status after the change: new,
contacted, quoted, booked, lost. |
| money | total, tax, discount,
discountCode, deposit, paid, balance. |
| booking | reference, date, time,
durationMinutes, endTime, acceptedAt. Null reference
until there is a booking. |
| documents | quoteNumber, invoiceNumber,
bookingReference. |
| links | Absolute URLs: the lead in the API and its three PDFs.
confirmationPdf is null before acceptance. |
| change | Only on events that have one. |
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.
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.
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"
}
}
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…
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
});
@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
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.
| Attempt | Waits | Elapsed |
|---|---|---|
| 1 | — | within ~15 s of the change |
| 2 | 30 s | 30 s |
| 3 | 1 min | 1.5 min |
| 4 | 2 min | 3.5 min |
| 5 | 4 min | 7.5 min |
| 6 | 8 min | 15.5 min |
| 7 | 16 min | 31.5 min |
| 8 | 32 min | 63.5 min — then abandoned |
X-PriceMyDay-Delivery is unique per event — store it and ignore
one you have seen.occurredAt if sequence matters.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_…"
| Endpoint | Returns |
|---|---|
| GET /api/admin/leads | Paged 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}/status | Move it in the pipeline.
Body {"status":"booked"}. |
| PATCH /api/admin/leads/{id}/assign | Assign to a team member. |
| POST /api/admin/leads/{id}/notes | Add a note. |
| GET /api/admin/leads/{id}/quote.pdf | The 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.csv | Everything, as a spreadsheet. |
The lead detail response uses the same money and booking
blocks as the webhook, so one set of types covers both.
lead.payment_received; do not expect to push one in.289100 means
₹289,100.00. Parse as decimal if you are summing them.Integration questions: [email protected].