API Integration Tutorial
This tutorial walks you through building a service integration that creates and launches campaigns via the Pidgr API. You’ll use the open source pidgr-proto package, which provides type-safe gRPC client stubs for Go and TypeScript.
Prerequisites
Section titled “Prerequisites”- A Pidgr organization with admin access
- An API key (created in the admin dashboard under Settings → API Keys)
- Go 1.21+ or Node.js 18+
Authentication: API Keys vs Passkeys
Section titled “Authentication: API Keys vs Passkeys”Pidgr enforces passkey authentication for all interactive users — both on the mobile app and the admin dashboard. Users cannot operate with email OTP alone; they must register a passkey (WebAuthn/FIDO2) during their first sign-in.
API keys are exempt from passkey enforcement. When you authenticate with an API key (pidgr_k_ prefix), the request bypasses the passkey check entirely. This is by design — service integrations, CI/CD pipelines, and MCP agents don’t have biometric capabilities.
| Auth method | Passkey required | Use case |
|---|---|---|
| JWT (interactive user) | Yes | Mobile app, admin dashboard |
| API key | No | Service integrations, MCP, CI/CD |
| SSO/SAML | Yes (after federation) | Enterprise users |
Option A: Go Integration
Section titled “Option A: Go Integration”1. Initialize Your Project
Section titled “1. Initialize Your Project”mkdir pidgr-integration && cd pidgr-integrationgo mod init example.com/pidgr-integration2. Install Dependencies
Section titled “2. Install Dependencies”go get github.com/pidgr/pidgr-proto/gen/go@latestgo get google.golang.org/grpc3. Create the Client
Section titled “3. Create the Client”package main
import ( "context" "crypto/tls" "fmt" "log"
pidgrv1 "github.com/pidgr/pidgr-proto/gen/go/pidgr/v1" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/metadata")
func main() { // Connect to the Pidgr API conn, err := grpc.NewClient("api.pidgr.com:443", grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})), ) if err != nil { log.Fatalf("failed to connect: %v", err) } defer conn.Close()
// Attach the API key to all requests apiKey := "pidgr_k_your_key_here" // use an environment variable in production ctx := metadata.AppendToOutgoingContext(context.Background(), "authorization", "Bearer "+apiKey, )
// Create service clients templateClient := pidgrv1.NewTemplateServiceClient(conn) campaignClient := pidgrv1.NewCampaignServiceClient(conn)
// 1. Create a template tmpl, err := templateClient.CreateTemplate(ctx, &pidgrv1.CreateTemplateRequest{ Name: "weekly-update", Title: "Weekly Engineering Update", Body: "Hi {{name}},\n\nHere's your weekly update:\n\n{{content}}", Variables: []*pidgrv1.TemplateVariable{ {Name: "name", Source: pidgrv1.TEMPLATE_VARIABLE_SOURCE_PROFILE, Required: true}, {Name: "content", Source: pidgrv1.TEMPLATE_VARIABLE_SOURCE_CUSTOM, Required: true}, }, }) if err != nil { log.Fatalf("failed to create template: %v", err) } fmt.Printf("Created template: %s (version %d)\n", tmpl.Template.Id, tmpl.Template.Version)
// 2. Create a campaign using the template campaign, err := campaignClient.CreateCampaign(ctx, &pidgrv1.CreateCampaignRequest{ Name: "Weekly Update - Week 12", Title: "Weekly Engineering Update", TemplateId: tmpl.Template.Id, TemplateVersion: tmpl.Template.Version, SenderName: "Engineering", Audience: []*pidgrv1.AudienceMember{ {UserId: "user-uuid-1", Variables: map[string]string{"content": "Sprint review on Friday."}}, {UserId: "user-uuid-2", Variables: map[string]string{"content": "Sprint review on Friday."}}, }, }) if err != nil { log.Fatalf("failed to create campaign: %v", err) } fmt.Printf("Created campaign: %s\n", campaign.Campaign.Id)
// 3. Launch the campaign started, err := campaignClient.StartCampaign(ctx, &pidgrv1.StartCampaignRequest{ CampaignId: campaign.Campaign.Id, }) if err != nil { log.Fatalf("failed to start campaign: %v", err) } fmt.Printf("Campaign started: %s (status: %s)\n", started.Campaign.Id, started.Campaign.Status)
// 4. Monitor deliveries deliveries, err := campaignClient.ListDeliveries(ctx, &pidgrv1.ListDeliveriesRequest{ CampaignId: campaign.Campaign.Id, }) if err != nil { log.Fatalf("failed to list deliveries: %v", err) } for _, d := range deliveries.Deliveries { fmt.Printf(" %s: %s\n", d.RecipientEmail, d.Status) }}Option B: TypeScript Integration
Section titled “Option B: TypeScript Integration”1. Initialize Your Project
Section titled “1. Initialize Your Project”mkdir pidgr-integration && cd pidgr-integrationnpm init -ynpm install typescript tsx @types/node --save-dev2. Install Dependencies
Section titled “2. Install Dependencies”The @pidgr/proto package is published on npm as a public package.
npm install @pidgr/proto @connectrpc/connect @connectrpc/connect-web @bufbuild/protobuf3. Create the Client
Section titled “3. Create the Client”import { createClient } from "@connectrpc/connect";import { createConnectTransport } from "@connectrpc/connect-web";import { CampaignService } from "@pidgr/proto/pidgr/v1/campaign_pb";import { TemplateService } from "@pidgr/proto/pidgr/v1/template_pb";import { TemplateVariableSourceSchema,} from "@pidgr/proto/pidgr/v1/common_pb";
// Create a transport with API key authenticationconst transport = createConnectTransport({ baseUrl: "https://api.pidgr.com", interceptors: [ (next) => async (req) => { const apiKey = process.env.PIDGR_API_KEY; // never hardcode keys req.header.set("authorization", `Bearer ${apiKey}`); return next(req); }, ],});
// Create service clientsconst templateClient = createClient(TemplateService, transport);const campaignClient = createClient(CampaignService, transport);
async function main() { // 1. Create a template const tmpl = await templateClient.createTemplate({ name: "weekly-update", title: "Weekly Engineering Update", body: "Hi {{name}},\n\nHere's your weekly update:\n\n{{content}}", variables: [ { name: "name", source: 1 /* PROFILE */, required: true }, { name: "content", source: 2 /* CUSTOM */, required: true }, ], }); console.log(`Created template: ${tmpl.template!.id} (version ${tmpl.template!.version})`);
// 2. Create a campaign using the template const campaign = await campaignClient.createCampaign({ name: "Weekly Update - Week 12", title: "Weekly Engineering Update", templateId: tmpl.template!.id, templateVersion: tmpl.template!.version, senderName: "Engineering", audience: [ { userId: "user-uuid-1", variables: { content: "Sprint review on Friday." } }, { userId: "user-uuid-2", variables: { content: "Sprint review on Friday." } }, ], }); console.log(`Created campaign: ${campaign.campaign!.id}`);
// 3. Launch the campaign const started = await campaignClient.startCampaign({ campaignId: campaign.campaign!.id, }); console.log(`Campaign started: ${started.campaign!.id} (status: ${started.campaign!.status})`);
// 4. Monitor deliveries const deliveries = await campaignClient.listDeliveries({ campaignId: campaign.campaign!.id, }); for (const d of deliveries.deliveries) { console.log(` ${d.recipientEmail}: ${d.status}`); }}
main().catch(console.error);4. Run It
Section titled “4. Run It”PIDGR_API_KEY=pidgr_k_your_key npx tsx main.tsWorking with the Proto Definitions
Section titled “Working with the Proto Definitions”The pidgr-proto repository is open source (Apache 2.0) and contains the canonical Protocol Buffer definitions for all Pidgr services.
Generated Code
Section titled “Generated Code”| Language | Package | Install |
|---|---|---|
| Go | github.com/pidgr/pidgr-proto/gen/go | go get github.com/pidgr/pidgr-proto/gen/go@latest |
| TypeScript | @pidgr/proto | npm install @pidgr/proto |
| Rust | Git dependency | pidgr-proto = { git = "...", tag = "v0.38.1" } |
Generating from Source
Section titled “Generating from Source”If you need custom generation (e.g., for a language not listed above), clone the proto definitions and run buf generate with your own plugins:
git clone https://github.com/pidgr/pidgr-proto.gitcd pidgr-proto
# Install buf (https://buf.build/docs/installation)# Modify buf.gen.yaml for your target languagebuf generateError Handling
Section titled “Error Handling”The API returns standard gRPC status codes. Handle errors appropriately in your integration:
import "google.golang.org/grpc/status"
resp, err := campaignClient.StartCampaign(ctx, req)if err != nil { st, ok := status.FromError(err) if ok { switch st.Code() { case codes.NotFound: log.Printf("campaign not found: %s", st.Message()) case codes.PermissionDenied: log.Printf("missing permission: %s", st.Message()) case codes.InvalidArgument: log.Printf("invalid request: %s", st.Message()) default: log.Printf("API error [%s]: %s", st.Code(), st.Message()) } }}import { ConnectError, Code } from "@connectrpc/connect";
try { await campaignClient.startCampaign({ campaignId: "..." });} catch (err) { if (err instanceof ConnectError) { switch (err.code) { case Code.NotFound: console.error("Campaign not found:", err.message); break; case Code.PermissionDenied: console.error("Missing permission:", err.message); break; default: console.error(`API error [${err.code}]: ${err.message}`); } }}Security Best Practices
Section titled “Security Best Practices”- Never hardcode API keys — use environment variables or a secrets manager
- Scope API keys — grant only the permissions your integration needs
- Rotate keys — create new keys periodically and revoke old ones
- Use separate keys per integration — isolate blast radius if a key is compromised
- Monitor usage — check
last_used_aton API keys to detect unused or suspicious activity
Next Steps
Section titled “Next Steps”- Explore the full API Reference for all available services
- Set up Webhooks to receive campaign delivery outcomes
- Install the MCP Server for AI agent integration