brainstorming
brainstorming
You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or planning anything new, in software or out of it (a talk, a business, a renovation).
appfolio-webhooks-eventsHandle AppFolio webhook events for property management notifications. Trigger: "appfolio webhook".
Independent GitHub snapshot · Automated classification · Original documentation governs
name: appfolio-webhooks-events description: 'Handle AppFolio webhook events for property management notifications.
Trigger: "appfolio webhook".
' allowed-tools: Read, Write, Edit, Bash(npm:), Bash(curl:), Grep version: 1.5.0 license: MIT author: Jeremy Longshore jeremy@intentsolutions.io tags:
AppFolio Stack delivers real-time webhook notifications for property management lifecycle events including tenant onboarding, lease execution, rent payments, and maintenance workflows. Use these webhooks to sync AppFolio data with your CRM, accounting system, or custom property management dashboards without polling the API.
// Use this only if the signed provider contract explicitly supports this API.
const response = await createVerifiedAppFolioClient().post("/webhooks", {
url: "https://yourapp.com/webhooks/appfolio",
events: ["tenant.created", "work_order.updated", "payment.received", "lease.signed"],
secret: process.env.APPFOLIO_WEBHOOK_SECRET,
});
import crypto from "crypto";
import { Request, Response, NextFunction } from "express";
function verifyAppFolioSignature(req: Request, res: Response, next: NextFunction) {
const signature = req.headers["x-appfolio-signature"] as string;
const expected = Buffer.from(crypto
.createHmac("sha256", process.env.APPFOLIO_WEBHOOK_SECRET!)
.update(req.body)
.digest("hex"));
const received = signature ? Buffer.from(signature) : Buffer.alloc(0);
if (received.length !== expected.length || !crypto.timingSafeEqual(received, expected)) {
return res.status(401).json({ error: "Invalid signature" });
}
next();
}
import express from "express";
const app = express();
app.post("/webhooks/appfolio", express.raw({ type: "application/json" }), verifyAppFolioSignature, (req, res) => {
const event = JSON.parse(req.body.toString());
// persistIncomingEvent performs a durable idempotency insert and queue write
// in one transaction. A failed persist must receive a retryable response.
persistIncomingEvent(event).then(() => {
res.status(202).json({ received: true });
}).catch(() => {
res.status(503).json({ error: "Event persistence unavailable" });
});
});
| Event | Payload Fields | Use Case |
|---|---|---|
tenant.created | tenant_id, property_id, email | Sync new tenant to CRM |
work_order.updated | work_order_id, status, assigned_vendor | Dispatch or escalate maintenance |
payment.received | lease_id, amount_cents, payment_method | Update accounting ledger |
lease.signed | lease_id, move_in_date, term_months | Activate unit and send welcome |
lease.expired | lease_id, unit_id, vacate_date | Trigger renewal or re-listing |
async function handleIdempotent(event: { id: string; type: string; data: any }) {
if (await durableEventStore.hasSucceeded(event.id)) return;
await routeEvent(event);
await durableEventStore.markSucceeded(event.id);
}
| Issue | Cause | Fix |
|---|---|---|
| Signature mismatch | Wrong secret or parsed body | Use express.raw() for verification |
| Duplicate events | AppFolio retry on timeout | Track event IDs for idempotency |
Missing property_id | Event from archived property | Check property status before processing |
| Durable store/queue unavailable | Cannot guarantee acknowledged event survives | Return retryable 503; do not acknowledge before persistence |
For a synthetic work-order update, deliver a valid signed event, a malformed
signature, a duplicate ID, and a downstream timeout. Prove the valid event is
persisted before 202, the malformed request is rejected without parsing, the
duplicate does not produce a second side effect, and the timeout remains queued
for reconciliation. If provider support, raw-body capture, signature secret,
durable store, or queue is unavailable, keep the endpoint disabled and use the
provider-approved polling/reconciliation path instead.
See appfolio-security-basics.