Webhook-Driven Pet Care: Four Events That Wake the Agent Behind PupPal — PupPal
2026-08-25
Webhook-Driven Pet Care: Four Events That Wake the Agent Behind PupPal — PupPal
Your dog has a routine, and every day of it produces a small moment: the morning photo by the food bowl, the sitter who arrives with a care code, the trip that ends and the code that dies. Most pet apps treat those moments as separate screens in an app. PupPal is webhook-driven pet care: it treats those moments as events. Four webhooks — puppal-init, puppal-photo, puppal-care-create, and puppal-care-end — are the entire nervous system of the product. Every photo you take, every care session you start, every trip you come home from is a message sent to the Hermes Agent, and the agent plus its memory are the body those messages animate.
This post is a tour of that webhook-driven design, grounded in the real Puppal source code: the Cloudflare Worker relay that fires each event, the payload each one carries, the skill each one wakes, and the memory keys each one writes. By the end you will see why a pet care product is better built as an event log than as a database of screens, and why the four events together add up to a dog that is known — not just recorded.
The Webhook Is the Product
It is worth starting with the shape of the whole system, because the webhooks are its skeleton. PupPal is a Flutter app talking to a Cloudflare Worker relay. The relay talks to a self-hosted Hermes Agent that does the actual thinking: vision analysis of photos, anomaly detection, care handbook generation, nightly reviews, and the daily voice message from your pet. The app and the relay meet over REST; the relay and the agent meet over webhooks. That asymmetry is deliberate. The app is a device you hold. Hermes is a service you host, often on a home machine that has no public address, no API gateway, and no reason to be polled from the internet every few seconds.
A webhook is the only boundary that makes sense there. The relay, which is always online, watches the product's real-world moments as they happen and translates each one into a POST request to {HERMES_WEBHOOK_BASE}/{endpoint} with an X-Webhook-Secret header. Hermes wakes, does its work, writes to memory, and answers. When Hermes is asleep or offline, the product degrades gracefully instead of breaking, because the events that matter — a photo filed, a care code issued — were already handled by the relay before the agent ever saw them.
The four events map cleanly onto the dog's real-world arc:
| Webhook | Triggered when | What wakes up | What gets written |
|---|---|---|---|
| puppal-init | A pet profile is created | Direct memory write | dog:{dog_id}:profile, dog:{dog_id}:state |
| puppal-photo | Anyone uploads a check-in photo | photo-analyze or care-monitor skill | dog:{dog_id}:state, dog:{dog_id}:photos |
| puppal-care-create | An owner starts a care session | care-handbook skill | dog:{dog_id}:care_sessions |
| puppal-care-end | A session ends, by hand or by clock | Memory finalization | session status, cleanup triggers |
Every one of these is a notification with coordinates, not a data dump. The payloads are tiny — a dog ID, a URL, a timestamp, a session ID — because Hermes already holds the deep knowledge in its memory. The webhook is the single authenticated moment where the relay's knowledge of who and Hermes's knowledge of how are allowed to meet. Two companion webhooks, puppal-state and puppal-timeline, serve that memory back to the app on demand. The four lifecycle events build it; the two read events display it.
puppal-init: The Event That Gives the Dog a Memory
Every dog in PupPal starts with the same moment: the owner finishes onboarding, the app calls POST /puppal-init on the relay, and the relay forwards the event to Hermes. One webhook. And with it, a dog that existed only as a photo on a phone becomes a living record in the agent's memory.
The payload is the completed pet profile, and its shape is worth studying because everything else in the product hangs off it:
{
"dog_id": "doudou",
"profile": {
"name": "豆豆",
"breed": "golden retriever",
"age_years": 3,
"weight_kg": 28,
"gender": "male",
"neutered": true,
"avatar_url": "https://r2.../avatar.jpg",
"feeding": {
"brand": "royal canin adult",
"amount_per_meal": "2 cups, about 280 g",
"times": ["07:00", "18:00"],
"notes": "wait 30 minutes after eating before walking"
},
"medications": [
{
"name": "ear drops",
"dosage": "3-4 drops per ear",
"schedule": "Monday, Thursday",
"notes": "massage the ear base gently for 30 seconds"
}
],
"allergies": ["chicken"],
"behavior": {
"friendly_with_strangers": true,
"leash_training": "good, but pulls at squirrels",
"commands_known": ["sit", "down", "wait", "come"],
"fears": ["vacuum cleaner", "thunder"],
"quirks": "carries socks to the living room"
},
"vet": { "name": "sunshine pet hospital", "phone": "...", "address": "..." },
"emergency_contact": { "name": "...", "phone": "..." },
"personality": { "tone": "warm", "traits": ["food-driven", "clingy", "timid"] }
}
}
The handler for puppal-init does exactly one thing, and it does it directly: it writes dog:{dog_id}:profile and dog:{dog_id}:state into memory. No skill, no model call, no analysis. This is the cheapest and most important webhook in the system, because it establishes the two keys that every later event reads. The profile is the dog's steady world — identity, feeding, medications, allergies, behavior, vet, emergency contact. The state is the dog's living picture — happiness, energy, health score, last photo time, photo count today. One webhook seeds both, and from that moment the agent has a subject: a particular animal with a particular routine, a known face, and a known baseline.
The design detail that matters here is that initialization is an event, not a REST response. The app sends puppal-init and gets back a confirmation; it does not wait for Hermes to finish anything, because there is nothing to finish. The profile lands in memory, and the memory is the source of truth for every subsequent event. Months later, when a care session starts, the care-handbook skill reads dog:{dog_id}:profile and produces instructions that mention the ear drops, the chicken allergy, and the sock-carrying quirk — all because one webhook, fired once at onboarding, planted the seed. Initialization is not a form submission; it is the birth certificate, filed with the agent that will spend the dog's whole life reading it.
puppal-photo: The Event That Thinks About Every Check-In
If puppal-init is the birth certificate, puppal-photo is the heartbeat. It is the webhook that fires on the product's core action — photo = check-in — and it is the one that does the heaviest work. The payload is small and honest:
{
"dog_id": "doudou",
"photo_url": "https://r2.example.com/photos/abc123.jpg",
"photo_id": "8f3c...",
"timestamp": "2026-06-18T14:30:00Z",
"source": "owner",
"caregiver_note": null,
"care_session_id": null
}
The photo itself never travels in the webhook. It lives in R2 under photos/{dog_id}/{photoId}.jpg, and the agent receives a URL. That one decision keeps the event small, keeps the binary out of the agent's hands, and gives the relay full control over who can fetch what. The source field is the routing switch: owner directs the event to the photo-analyze skill, while caregiver directs it to care-monitor, the sharper-eyed variant used during someone else's care session. One endpoint, one event, two very different reactions depending on who is holding the phone.
When Hermes receives puppal-photo with source=owner, the photo-analyze skill runs a fixed pipeline. First it verifies the X-Webhook-Secret header against its own WORKER_WEBHOOK_SECRET — a mismatched secret gets a 401 and not a single pixel is analyzed. Then it reads dog:{dog_id}:state from memory to get the current baseline. Then it sends the photo URL to the Vision model in a single call that returns V1 basic recognition, V2 posture, V4 expression, and V5 environment in one shot. Then a pure-Python script runs C7 anomaly detection against that result and the last week of history. And finally — the part that makes the webhook a memory engine — it writes back:
dog:{dog_id}:stateis merged with new happiness and energy values, computed as exponential moving averages with a 0.3 weight on the newest photo.last_photo_atandphoto_count_todayare updated.- The full analysis record — vision tags, anomalies, caregiver note, timestamp — is appended to
dog:{dog_id}:photos.
The response flows back through the relay to the app: a human-readable message ("Doudou looks happy! Resting on the living room floor. All normal"), a state snapshot with happiness, energy and health score, and a trend brief. If anomaly_score crosses 0.5 the response carries a warning; above 0.7 the agent pushes an alert over WeChat immediately; a critical anomaly pushes instantly, no waiting. Every one of those thresholds is a behavior of the event, not of the app — the photo app just takes the photo, and the webhook decides what it means.
When source=caregiver, the same event lands in care-monitor instead, and the difference is the whole point of the care product. The skill first verifies the care session is still active in memory — a photo from an ended session is politely rejected. Then it runs the same vision pipeline but adds care-specific checks: the first anomaly of the session is flagged on sight, a repeated anomaly type upgrades in severity, a caregiver note containing keywords like "not eating" or "diarrhea" triggers a high-severity flag immediately, and three consecutive non-positive expressions raise an alert. All standard thresholds drop by 40 percent — the push threshold from 0.7 to 0.42, the health-drop alert from 0.15 to 0.09 — and alerts go straight to the owner instead of waiting for the nightly review. The event carries a care_session_id, and the skill appends each analysis to dog:{dog_id}:care_sessions.{id}.caregiver_photos. A stranger is caring for your dog, so the same event that says "everything is fine" is tuned to be suspicious on your behalf.
Why Webhook-Driven Design Beats Polling
The obvious alternative to all of this is polling: the app asks the agent every few minutes whether anything new has happened. PupPal rejects that, and the rejection is a design position, not an accident. Four properties of the domain make webhooks the right call.
The agent is self-hosted and intermittently awake. Hermes lives on hardware the owner controls, often a home machine that sleeps at night. Polling assumes an always-on, always-answering service; webhooks assume a listener that may be absent and a sender that does not care. The relay's tryHermesWebhook wrapper swallows every failure and returns an empty result, so creating a care session, verifying a PIN, and ending a session all succeed with Hermes unreachable. A care code degrades gracefully because the handbook snapshot was already captured at creation time. The product's core promises never depend on a model being online at the exact moment a photo is taken.
Events are the units of truth. A check-in is not a row that needs reading; it is something that happened, with a timestamp and a source. Firing a webhook at the moment of the event, and having the agent append it to memory, produces a timeline that is the dog's actual history — not a reconstruction. The nightly daily-review cron later reads dog:{dog_id}:photos and dog:{dog_id}:state, computes S1 baseline and S3/S4 feeding and activity comparisons, and appends to dog:{dog_id}:stats.history. It never polls the app; it reads the record the events left behind. The daily voice at 09:00 does the same, narrating yesterday from yesterday's events. Webhooks turn every product action into memory automatically; polling turns every product action into a round trip that must be remembered manually.
The relay, not the agent, stays online. The Worker is the always-available front door: it validates uploads, stores photos in R2, checks rate limits in KV, and only then forwards what matters. A caregiver photo with a forged dog_id field is corrected by the relay from the care token before the webhook is ever built. The staging path even handles the case where the owner is fully offline: a caregiver's photo is stored, marked staged, and recorded in D1 so the owner can pull it later and run the analysis locally. The agent's absence is an input to the design, not a failure case.
Asynchronous boundaries keep the UI fast. The webhook response returns the analysis result to the app in one round trip, but nothing in the app blocks on the agent. The relay answers CORS preflights, applies its coarse per-IP rate limit, and dispatches routes before the agent is ever involved. The app shows the photo instantly; the meaning arrives when the event completes. Users feel the speed as product quality, but it is actually an architectural consequence of event-driven, not request-driven, thinking. For the full walkthrough of what happens after the shutter, the photo check-in deep dive follows the same bytes from camera to memory.
puppal-care-create: The Event That Opens the Door
The third event is where the sharing model begins. When the owner taps "start care" for a weekend away, the relay does four things in order: it validates the request, generates the credentials, stores the session, and fires puppal-care-create.
The credentials follow the RustDesk pattern — a nine-digit care code grouped 3-3-3 so it can be read aloud over the phone, and a four-digit PIN that is hashed with SHA-256 plus a server secret before it ever touches storage. The session record, with its dog ID, session ID, PIN hash, window, and transfer mode, is written to KV under care:{careCode} with a thirty-day TTL and mirrored into D1 for durability. Only then does the webhook fire:
{
"dog_id": "doudou",
"care_session_id": "care_abc123",
"start_time": "2026-06-18T10:00:00Z",
"end_time": "2026-06-20T18:00:00Z",
"care_code": "908 174 553"
}
No photos, no profile dump, no instructions — just coordinates. The agent that receives it knows exactly where to find everything else. The care-handbook skill wakes, reads dog:{dog_id}:profile and dog:{dog_id}:state, and assembles the care document: eight sections from basic info through feeding and medication to emergency instructions and check-in guidance, with the iron rule that medication, feeding, vet, and emergency numbers are copied verbatim from the profile and never "optimized" by the model. The handbook returns as Markdown, the relay stores it as a snapshot in the session record — the client-confirmed preview winning over the agent's draft — and the event's memory write closes the loop: dog:{dog_id}:care_sessions gains an entry with status active, a handbook_generated_at timestamp, the session window, and an empty caregiver_photos array waiting to be filled.
The door the event opens works without accounts. The sitter opens the H5 page, types the nine-digit code and the PIN, and the relay verifies the hash with a timing-safe comparison, throttles failures to five per minute, locks the code for an hour after ten wrong attempts, and issues a short-lived upload token capped at the lesser of the session's remaining time and seven days. A companion webhook, puppal-care-get, tries to fetch a fresh handbook from Hermes at verify time; if the agent is down, the KV snapshot from creation time is served instead. The care codes and PIN post covers that security model in depth. From the event's point of view, one message to the agent bought the whole weekend: the dog's knowledge turned into instructions, the instructions turned into a snapshot, and the snapshot outlived any single machine in the chain — exactly what a care document must do.
puppal-care-end: The Event That Closes the Loop
Every session ends, and the fourth event is the closing ceremony. puppal-care-end carries only three fields — dog_id, care_session_id, and reason — and reason is always either manual or expired.
The manual path runs when the owner taps "end care" in the app. The relay marks the session ended in KV, updates the D1 record, and fires the webhook. On the agent side, the handler updates memory: dog:{dog_id}:care_sessions.{id}.status flips to ended, and the care code becomes invalid on the spot. No lingering half-open sessions, no sitter discovering a code that still works after the owner is home. The sitter's upload token dies with the session; a photo attempt after the end event returns a rejection instead of an analysis.
The expired path is where the design shows its patience. The worker declares a cron trigger, 0 * * * *, and every hour expireCareSessions lists every care: key in KV, finds sessions whose end_time has passed while still active, marks them ended, mirrors the change to D1, and fires puppal-care-end with reason: "expired" — the agent's memory is finalized even when no human pressed the button. Then the cleanup: R2 photo binaries for sessions ended more than seven days ago are deleted, while their metadata rows stay in D1 for the record, and the KV session key expires by its own TTL. Photos live as long as they are useful and no longer.
The symmetry is the point. The same four-field event that opened the door closes it, and closing is as automated as opening: the owner's hand, the sitter's departure, or the calendar itself all reduce to one message, and the agent's memory is always left consistent. The daily health review post shows how the review cron then folds a ended care period into the dog's long-term stats — the events all feed one continuous record.
What Webhook-Driven Design Means for Your Dog
Step back from the pipeline and the four events tell a single story. Init makes the dog known. Photo keeps the dog known, photo by photo, with the same photo meaning a check-in for the owner and a surveillance pass for the sitter. Care-create expands the circle of people who can act on that knowledge, without a single account. Care-end closes the circle and files the whole episode into the dog's record. Four webhooks, and a dog that is never unmonitored, never unremembered, and never dependent on one machine being awake.
That is the real product. PupPal is not a photo album with a chatbot bolted on; it is an event log whose byproduct is a living pet profile. Every check-in photo is an event that updates state and appends history. Every care session is an event that generates instructions and then archives them. Every trip ends in an event that finalizes memory and cleans up bytes. The webhooks are the design, and the agent's memory — dog:{dog_id}:profile, :state, :photos, :stats.history, :care_sessions — is the record they keep. If you want to see the agent that wakes on those events, the Hermes Agent skills architecture post walks all five skills from the receiving side.
For an owner, webhook-driven pet care means the difference between an app you open and a system that listens. Your morning photo arrives, and an agent that knows your dog reads it against yesterday, against last week, against the baseline set at onboarding. Your sitter's photos arrive under sharper thresholds, and you hear about a problem while it is still a photo, not a phone call. Your care session ends, and the whole episode folds into the record the nightly review reads. Nothing waits to be asked. Everything happens because something happened.
So try it with your own dog's morning. Point PupPal at the bowl, take the photo, and know that one event just updated a state, appended a history, and checked a baseline — before you have even put the phone down. One photo a day is one event a day, and a dog's life is made of them. The agent is always listening; the webhooks just make sure it never misses a moment.