Developer documentation
API integration guide
Server-side examples for integrating transactional and lifecycle email.
npm install @blitzglobaltech/pulsepigeonimport { 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
| Step | Rule |
|---|---|
| Create project | POST /v1/projects with an Idempotency-Key for the setup operation. |
| Create API key | POST /v1/api-keys and store the one-time secret immediately. |
| Send message | POST /v1/messages; supply an Idempotency-Key header per business event for safe retries. |
| Track status | Use 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"
}'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 FalsePersonalized 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.csvError handling
| Status | Meaning | Action |
|---|---|---|
| 202 | Accepted | Track final state through logs or webhooks. |
| 409 | Idempotency conflict | Use the same body or generate a new key. |
| 423 | Sending halted | Review your workspace sending status and contact support if sending remains paused. |
| 429 | Rate limited | Back off until tenant or provider capacity resets. |