SendBeam

Windmill setup

Connect SendBeam to Windmill: what you need, the steps, and the code.

View as Markdown

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

  1. Create an API key. Settings → API keys in SendBeam, with contacts:write and transactional:send (and lists:write if you add a list step: POST /api/v1/lists/{id}/contacts with the same helper). The full key is shown once.
  2. Store it as a resource. In Windmill, add a resource of type c_sendbeam with 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 an api_key field works; the name below is a suggestion.)
  3. Add a script. Copy either file below into a new script. Windmill infers the dependencies (requests for Python; nothing beyond fetch for TypeScript) and generates the input form from the signature.
  4. 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/contacts answers 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/transactional needs no marketing consent.

← Back to the Windmill integration