Webhooks
Rather than ask Foudra over and over whether anything has changed, your program can be told. An admin adds a webhook under Webhooks in the app: an https address of yours and the events to send there. Foudra then POSTs a JSON message to that address each time one of those events happens. The reference lists the events and what each carries.
A receiver
Three things matter, and this receiver, standard library only, does all three:
- Check the signature, so you act only on messages that came from Foudra.
- Answer at once with
2xx, and do the work afterwards. No answer within 10 seconds counts as a failure and is tried again later. - Expect repeats. A message can arrive twice; remember the event ids you've handled.
import hashlib
import hmac
import json
import os
import queue
import threading
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
SECRET = os.environ["FOUDRA_WEBHOOK_SECRET"] # shown once, when the webhook was made
work = queue.Queue()
seen = set() # in real life, a table: it must survive a restart
def verify(header, body):
"""X-Foudra-Signature is t=<unix time>,v1=<hex HMAC-SHA256 of "<t>.<body>">."""
try:
parts = dict(p.split("=", 1) for p in header.split(","))
t = int(parts["t"])
except (KeyError, ValueError):
return False
expected = hmac.new(SECRET.encode(), str(t).encode() + b"." + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", "")) and abs(time.time() - t) < 300
class Receiver(BaseHTTPRequestHandler):
def do_POST(self):
body = self.rfile.read(int(self.headers.get("Content-Length", 0)))
if not verify(self.headers.get("X-Foudra-Signature", ""), body):
self.send_response(401)
self.end_headers()
return
work.put(json.loads(body))
self.send_response(204)
self.end_headers()
def worker():
while True:
event = work.get()
if event["id"] in seen:
continue
seen.add(event["id"])
if event["type"] == "work_order.completed":
wo = event["data"]["work_order"]
print("done:", wo["number"], wo["external_ref"])
elif event["type"] == "stock.changed":
print(event["text"])
threading.Thread(target=worker, daemon=True).start()
ThreadingHTTPServer(("", 8000), Receiver).serve_forever()
Put it behind your web server's https, add the webhook with its address, and press Send test: a ping event arrives, and the webhook's page shows the answer it got.
Check the signature over the body exactly as received, before parsing it: parsing and re-encoding the JSON changes the bytes.
When your receiver is down
Foudra tries again after 1 minute, 5, 15, an hour, 3, 6 and 12 hours, then gives up on that message. After eight failures in a row the webhook is switched off and a task on the dashboard says so. The webhook's page shows each delivery and lets an admin send one again.
Because of retries, a message can arrive after a later one. If order matters, use occurred_at; if you must not miss anything, also read the current state from the API now and then, rather than relying on every message arriving.