PulsePigeonemail infrastructure

SDKs and REST

Choose an integration

SDK detailsAPI referenceGlossary
npm install @blitzglobaltech/pulsepigeon
import { PulsePigeon } from "@blitzglobaltech/pulsepigeon";

const client = new PulsePigeon(process.env.PULSEPIGEON_API_KEY!);

await client.messages.send({
  project_id: "proj_123",
  from_email: "[email protected]",
  to: [{ email: "[email protected]" }],
  subject: "Welcome",
  text: "Hello from PulsePigeon."
}, { idempotencyKey: "welcome-user-123" });

Idempotency support

POST /v1/messages accepts an optional Idempotency-Key header. When you supply one, retrying the same key with the same body returns the original accepted result instead of sending again — strongly recommended for any retry loop or distributed send path.

Integration path

StepRule
Create projectPOST /v1/projects with an Idempotency-Key for the setup operation.
Create API keyPOST /v1/api-keys and store the one-time secret immediately.
Send messagePOST /v1/messages; supply an Idempotency-Key header per business event for safe retries.
Track statusUse message logs or signed status webhooks for final delivery state.

Send example

curl -X POST "$PULSEPIGEON_API_URL/v1/messages" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: password-reset-user-123-event-456" \
  -H "Authorization: Bearer $PULSEPIGEON_API_KEY" \
  -d '{
    "project_id":"proj_123",
    "from_email":"[email protected]",
    "to":[{"email":"[email protected]"}],
    "subject":"Reset your password",
    "text":"Use the secure reset link in this email.",
    "category":"security"
  }'

Quickstarts

From zero to first send

Webhook verificationSDK reference

The fastest path is cURL against the REST API. Framework-specific snippets below show how the same call looks inside Next.js and FastAPI; the runnable, test-covered examples live next to the SDKs (sdk/typescript/examples/, sdk/python/examples/).

cURL

Run these commands in a POSIX shell with cURL. Use a signed workspace session with manage_projects and manage_api_keys permission for setup; API-key creation also requires recent MFA. A pp_live_... or pp_test_... API key cannot create the project or another API key. Set PULSEPIGEON_SESSION_TOKEN to the signed token returned by your workspace sign-in flow after completing its MFA challenge. Copy the project ID and one-time API key secret from the responses as indicated. Keep credentials out of prompts, gists, and captured logs.

Run each block separately, in order. The later blocks refuse to run until the value from the previous response is exported, so pasting everything at once cannot send a request with a missing ID or key.

# 1a. Create the project (one-time setup)
export PULSEPIGEON_API_URL="${PULSEPIGEON_API_URL:-https://api.pulsepigeon.com}"
# Set PULSEPIGEON_SESSION_TOKEN to your MFA-verified workspace session token.
curl -X POST "$PULSEPIGEON_API_URL/v1/projects" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $PULSEPIGEON_SESSION_TOKEN" \
  -H "Idempotency-Key: setup-project-$(date +%s)-$RANDOM" \
  -d '{"name":"My integration"}'
# Then set the project ID from the response:
#   export PULSEPIGEON_PROJECT_ID=<id from the response>
# 1b. Create the API key (shown exactly once)
curl -X POST "$PULSEPIGEON_API_URL/v1/api-keys" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $PULSEPIGEON_SESSION_TOKEN" \
  -H "Idempotency-Key: setup-key-$(date +%s)-$RANDOM" \
  -d "{\"name\":\"Send key\",\"project_id\":\"${PULSEPIGEON_PROJECT_ID:?export PULSEPIGEON_PROJECT_ID from step 1a}\",\"scopes\":[\"messages:send\"]}"
# Then store the one-time secret without echoing it, and keep it out of logs:
#   read -rs PULSEPIGEON_API_KEY && export PULSEPIGEON_API_KEY
# 2. Send (Idempotency-Key per business event for safe at-least-once delivery)
curl -X POST "$PULSEPIGEON_API_URL/v1/messages" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ${PULSEPIGEON_API_KEY:?export PULSEPIGEON_API_KEY from step 1b}" \
  -H "Idempotency-Key: welcome-user-123-event-456" \
  -d "{\"project_id\":\"${PULSEPIGEON_PROJECT_ID:?export PULSEPIGEON_PROJECT_ID from step 1a}\",\"from_email\":\"[email protected]\",\"to\":[{\"email\":\"[email protected]\"}],\"subject\":\"Welcome\",\"text\":\"Hello.\"}"

Next.js (TypeScript)

A route handler that accepts a JSON body, returns a 202 with the assigned message ID, and passes the request through the official TypeScript SDK. Pair this with webhook verification to follow delivery outcomes. Replace the failing application auth placeholder with a check that authenticates the caller and authorizes sends for the requested project before forwarding.

import { NextResponse } from "next/server";
import { PulsePigeon } from "@blitzglobaltech/pulsepigeon";

const client = new PulsePigeon(process.env.PULSEPIGEON_API_KEY!, {
  baseUrl: process.env.PULSEPIGEON_API_URL
});

export async function POST(request: Request) {
  if (!(await authorizeSend(request))) {
    return NextResponse.json({ error: "forbidden" }, { status: 403 });
  }
  const body = await request.json();
  if (!(await authorizeProjectSend(request, body.projectId))) {
    return NextResponse.json({ error: "forbidden" }, { status: 403 });
  }
  const accepted = await client.messages.send(
    {
      project_id: body.projectId,
      from_email: body.fromEmail,
      to: [{ email: body.to }],
      subject: body.subject,
      text: body.text,
    },
    { idempotencyKey: `send-${body.projectId}-${body.eventId}` },
  );
  return NextResponse.json({ id: accepted.id }, { status: 202 });
}

async function authorizeSend(_request: Request): Promise<boolean> {
  // Replace with your application authentication check.
  return false;
}

async function authorizeProjectSend(_request: Request, _projectId: string): Promise<boolean> {
  // Replace with your application authorization check for this project.
  return false;
}

FastAPI (Python)

An ASGI endpoint that accepts a JSON body and forwards it through the official Python client. Pair this with webhook verification to track delivery outcomes. Replace the failing application auth placeholder with a check that authenticates the caller and authorizes sends for the requested project before forwarding.

import os

from fastapi import FastAPI, HTTPException, Request
from pydantic import BaseModel
from pulsepigeon_client import PulsePigeonClient

app = FastAPI()
client_options = {"api_key": os.environ["PULSEPIGEON_API_KEY"]}
if api_url := os.environ.get("PULSEPIGEON_API_URL"):
    client_options["base_url"] = api_url
client = PulsePigeonClient(**client_options)

class SendRequest(BaseModel):
    project_id: str
    from_email: str
    to: str
    subject: str
    text: str
    event_id: str

@app.post("/messages")
def send_message(req: SendRequest, request: Request) -> dict[str, str]:
    if not authorize_send(request, req.project_id):
        raise HTTPException(status_code=403, detail="forbidden")
    accepted = client.send_message(
        {
            "project_id": req.project_id,
            "from_email": req.from_email,
            "to": [{"email": req.to}],
            "subject": req.subject,
            "text": req.text,
        },
        idempotency_key=f"send-{req.project_id}-{req.event_id}",
    )
    return {"id": accepted["id"]}

def authorize_send(_request: Request, _project_id: str) -> bool:
    # Replace with your application authentication and project authorization.
    return False

Personalized transactional batches

POST /v1/messages/batch accepts 1 to 100 items and at most 10 MB of decoded JSON. Every item requires a unique idempotency_key and can contain an inline message or a published template with its own recipient and variables. The response uses partial atomicity: accepted items include their message IDs, while rejected items include an HTTP-style status and error code. Retry the same items with the same keys to replay accepted results without duplicate sends and retry previously failed work.

curl -X POST "$PULSEPIGEON_API_URL/v1/messages/batch" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $PULSEPIGEON_API_KEY" \
  -d '{
    "items":[{
      "idempotency_key":"invoice-123-user-456",
      "message":{
        "project_id":"proj_123",
        "from_email":"[email protected]",
        "to":[{"email":"[email protected]"}],
        "subject":"Your invoice",
        "text":"Invoice 123 is ready."
      }
    }]
  }'

Webhook signature

Every status-webhook delivery carries a PulsePigeon-Signature header with a Unix timestamp and an HMAC-SHA256 digest over the canonical JSON body. See the webhook verification guide for the full algorithm, the TypeScript and Python SDK snippets, golden test vectors, and the rotation procedure.

PulsePigeon-Signature: t=1783380000,v1=<hex-hmac>
signed_payload = "<timestamp>.<canonical-json-body>"

Suppression import

curl -X POST "$PULSEPIGEON_API_URL/v1/suppressions/import" \
  -H "Content-Type: text/csv" \
  -H "Authorization: Bearer $PULSEPIGEON_API_KEY" \
  --data-binary @suppressions.csv

Error handling

StatusMeaningAction
202AcceptedTrack final state through logs or webhooks.
409Idempotency conflictUse the same body or generate a new key.
423Sending haltedReview your workspace sending status and contact support if sending remains paused.
429Rate limitedBack off until tenant or provider capacity resets.