Part 2 of 7 · Review request sender series ~8 min read

How a completed job triggers a request

Before a single review request exists, the system has to turn a raw ‘job done’ event into something safe to act on later: prove it’s real, prove it hasn’t already made a request for this job, check the customer hasn’t opted out, and capture the signal that will decide the ask. This post is about that gate — how a completion webhook becomes one scheduled, send-once request.

Key takeaways

  • The trigger is a completion webhook from your booking or jobs tool, posted to one Lambda Function URL — no API Gateway.
  • The webhook is verified by its signature before anything else runs, so a stranger can’t make the system pester your customers.
  • A conditional write keyed on the job guarantees one request per completed job — a re-marked job or a retried webhook never asks twice.
  • The opt-out list is checked here, before a request is even scheduled, so a customer who opted out never enters the pipeline.
  • Whatever signal the customer already left — a rating, a reply, prior sentiment — is captured now and carried with the request.

From “job done” to a request

Everything starts with one event the business already generates but rarely does anything with: a job marked complete. Booking systems and field-service apps expose this as a webhook — when an order ships, an appointment is closed out, or someone taps “complete”, the tool sends a small HTTP POST with the customer, the job, a completion id, and often a rating or a note. That POST lands on a single Lambda Function URL. There’s no API Gateway in front of it; a Function URL is a plain HTTPS endpoint on the function itself, which is all a webhook needs and the cheapest way to receive one.

The catch function’s job is not to send an ask. It’s to decide whether an ask is warranted at all, and to turn the event into exactly one request scheduled for later. Most of this post is about the checks that sit between “a job finished” and “a request is on the clock”, because that gap is where a review system either earns trust or becomes the thing that texts a customer four times about the same haircut.

Prove it’s real, then prove it’s new

The first check is authenticity. A Function URL is public, so the first thing the function does is verify the tool’s signature — a hash of the request body signed with a shared secret held in Secrets Manager. If the signature doesn’t match, the request is dropped with a 401 and nothing else happens. Without this, anyone who found the URL could fabricate “completed jobs” and make the system message strangers; with it, only your booking tool can start a request.

The second check is duplication, and it’s the one that keeps the system from nagging. A job can be marked complete more than once — an engineer re-opens and re-closes a ticket, an order flips to complete then back and forth, or the tool retries a webhook it thinks failed. Either way, the system must make exactly one request per completed job. So the function writes a request record keyed on the completion id, using a conditional write to DynamoDB: the first event for that job wins and creates the request; any repeat sees the existing record and stops. One finished job, one ask, however many times the status was touched.

Should we ask at all?

Two more gates decide whether a request should even be created. Opt-out is the suppression list: any customer who has ever replied STOP, unsubscribed, or been marked “do not contact” is recorded as suppressed, and the system will never message that contact again. It’s checked here, at the front door, so an opted-out customer never even becomes a scheduled request — and it’s checked again at send time in Part 3, because someone can opt out during the day the request is waiting. Eligibility is the second: some jobs shouldn’t generate an ask at all — a warranty callback, a cancelled order, an internal job, or a customer asked for a review in the last few months already. Those are recorded and dropped, because the cheapest ask to handle is the one you correctly decide not to make.

Only once a job is verified, de-duplicated, not opted out, and eligible does the function look at how the customer felt — and capture the signal that Part 5 will later grade.

The catch gate: completion webhook in, verify, one-per-job guard, opt-out and eligibility checks, capture signal, then schedule On the left, an external box “Booking / jobs tool” sends a completion webhook by an arrow into a Lambda Function URL box labelled the catch function, inside a dotted AWS account container. From the catch function a vertical sequence of decision steps runs downward: first “Verify signature”, reading a shared secret from a Secrets Manager box; a failed signature exits to a small “Drop 401” box. Next “New job? (one per job)”, backed by a DynamoDB requests table with a conditional write on the completion id; a repeat exits to a “Stop, already asked” box. Next “Opt-out / eligible”, reading an opt-out list and settings; an opted-out or ineligible job stops entirely. Next “Capture signal” — the rating, reply, or prior sentiment carried on the event or read from a customers table — stored on the request. Finally an arrow into an EventBridge Scheduler box labelled schedule one ask for later. A note reads: nothing is scheduled until a job is proven real, proven new, not opted out, and eligible — one ask per completed job. Jobs tool completion webhook AWS account Catch function Function URL Verify signature Secrets Manager shared secret Drop 401 bad sig New job? one per completion id DynamoDB requests conditional write Stop, already asked dup Opt-out / eligible suppress or skip Capture signal rating, reply, history DynamoDB customers prior sentiment Schedule one ask EventBridge, for later Nothing is scheduled until a job is real, new, not opted out, and eligible — one ask per completed job.
Fig 2. The catch gate. A completion webhook is verified by signature, guarded to one request per job with a conditional write, checked against the opt-out list and eligibility rules, and stamped with whatever signal the customer left — only then is a single ask scheduled for later.

Capturing the signal

The last thing the catch function does before scheduling is gather the signal — the raw material Part 5 will grade to decide happy from unhappy. There are three sources, in order of usefulness. Best is a signal the customer just gave: many booking tools collect a quick star rating or a one-line note at the point of completion, and if that’s on the event it’s the strongest read we’ll get. Next is anything the customer said during the job — a reply, a message thread, a note the engineer logged. Last, if there’s no fresh signal at all, is prior sentiment: the customer’s history in the mirrored records, so a long-standing regular who’s always been happy is read differently from someone whose last two visits went sideways.

All of it is captured now and stored on the request, but crucially it is not graded here. Grading is a soft judgement that belongs at send time, next to the compose step, and deferring it keeps this front-door function fast and deterministic: it does signature, dedup, opt-out, eligibility, and capture, and hands off. If a job carries no signal whatsoever — a boiler service where the engineer logged nothing and the customer said nothing — the request is still made, marked unclear, and Part 5 handles that band deliberately rather than guessing. Everything eligible becomes a clean request in DynamoDB: customer, job, contact, the captured signal, and the completion id, ready for Part 3 to put on the clock.

Design rules that shaped the catch step

  • One public surface. A single Lambda Function URL receives the completion webhook; there is no API Gateway and no other way in.
  • Verify before you trust. A bad signature is dropped with a 401 before any work — the URL being public is fine because the secret isn’t.
  • One job, one request. A conditional write on the completion id collapses re-marks and retries into a single scheduled ask.
  • Decide not to ask, cheaply. Opt-out and eligibility are checked before scheduling — a suppressed or ineligible job costs nothing.
  • Capture, don’t grade. The signal is gathered here and graded later, next to compose, so the front door stays fast and deterministic.
  • No signal is still a signal. A job with nothing attached becomes an unclear request, handled deliberately rather than guessed about.
All posts