Windmill setup
Connect SendBeam to Windmill: what you need, the steps, and the code.
Via API
Windmill turns a script into a job with a UI, a schedule and an audit trail. SendBeam is a JSON API behind one header, so the script that adds a customer, tags a buyer or sends an order confirmation is the same fetch you would write anywhere — with the API key held as a Windmill resource rather than in the code.
Before you start
- A Windmill workspace (cloud or self-hosted)
- A SendBeam API key with the permissions the script needs
- A verified sending domain, for site email
Set it up
- Create an API key. Settings → API keys in SendBeam, with
contacts:writeandtransactional:send(andlists:writeif you add a list step:POST /api/v1/lists/{id}/contactswith the same helper). The full key is shown once. - Store it as a resource. In Windmill, add a resource of type
c_sendbeamwith one field,api_key, holding the key. Scripts receive it as an argument, so the key never sits in source or in a job’s inputs. (Any resource type with anapi_keyfield works; the name below is a suggestion.) - Add a script. Copy either file below into a new script. Windmill infers the dependencies (
requestsfor Python; nothing beyondfetchfor TypeScript) and generates the input form from the signature. - Run it once with your own address, check the contact and the email in SendBeam, then schedule it or wire it into a flow.
Code
TypeScript (Bun or Deno)
sendbeam_add_contact.ts
type Sendbeam = { api_key: string };
async function call(sb: Sendbeam, url: string, body: unknown) {
const res = await fetch(url, {
method: "POST",
headers: { "x-api-key": sb.api_key, "content-type": "application/json" },
body: JSON.stringify(body),
});
if (res.status === 409) return res.json(); // already a contact / already on the list
if (!res.ok) throw new Error(`SendBeam ${url} → ${res.status}: ${await res.text()}`);
return res.json();
}
export async function main(
sendbeam: Sendbeam,
email: string,
name: string = "",
welcome: boolean = false,
) {
const [first, ...rest] = name.trim().split(/\s+/).filter(Boolean);
const contact = await call(sendbeam, "https://sendbeam.io/api/v1/contacts", {
email, first_name: first, last_name: rest.join(" ") || undefined, source: "windmill",
});
if (welcome) {
await call(sendbeam, "https://sendbeam.io/api/v1/transactional", {
to: { email, name },
subject: "Welcome aboard",
html: `<p>Hi ${first ?? "there"}, thanks for signing up.</p>`,
});
}
return contact;
}
Python
sendbeam_add_contact.py
import requests
def _call(api_key: str, url: str, body: dict) -> dict:
res = requests.post(
url,
headers={"x-api-key": api_key, "content-type": "application/json"},
json=body,
timeout=30,
)
if res.status_code == 409: # already a contact / already on the list
return res.json()
res.raise_for_status()
return res.json()
def main(sendbeam: dict, email: str, name: str = "", welcome: bool = False) -> dict:
api_key = sendbeam["api_key"]
parts = name.split()
first, last = (parts[0] if parts else None), (" ".join(parts[1:]) or None)
contact = _call(api_key, "https://sendbeam.io/api/v1/contacts", {
"email": email, "first_name": first, "last_name": last, "source": "windmill",
})
if welcome:
_call(api_key, "https://sendbeam.io/api/v1/transactional", {
"to": {"email": email, "name": name},
"subject": "Welcome aboard",
"html": f"<p>Hi {first or 'there'}, thanks for signing up.</p>",
})
return contact
Receiving SendBeam events
Every Windmill script has a webhook URL (the script’s page → Webhooks). Add it under Settings → Webhooks in SendBeam, choose the events, and Windmill runs the script with the event body as its arguments. Verify the signature at the top of the script:
import hashlib, hmac, time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(kv.split("=", 1) for kv in header.split(","))
t = int(parts.get("t", "0"))
if abs(time.time() - t) > 300:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))
Use the “raw body” webhook variant so the bytes you sign are the bytes SendBeam sent.
Things to know
- A resource per environment keeps a staging run away from your production list.
POST /api/v1/contactsanswers 409 for an address that already exists; both scripts treat that as success so a re-run is safe.- Windmill’s flow steps can pass one script’s return value (the contact) into the next, so “add, put on a list, then send” is three small scripts rather than one large one.
- Only add people to a marketing list if they agreed to hear from you. Site email through
/api/v1/transactionalneeds no marketing consent.