Pular para o conteúdo

API Integration Tutorial

Este conteúdo não está disponível em sua língua ainda.

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 client stubs for Go and TypeScript.

The API speaks gRPC and the Connect protocol over the same endpoint. That means you can call it with native gRPC clients, Connect clients, or — because Connect supports JSON over plain HTTP — with nothing more than curl.

  • A Pidgr organization with admin access
  • An API key (created in the admin dashboard under SettingsAPI Keys)
  • Go 1.22+ or Node.js 18+

Interactive users sign in at auth.pidgr.com (passwordless — email one-time codes and passkeys). Machine access uses one of two credentials:

Credential Best for How it’s scoped
Scoped API key (pidgr_k_) Server-to-server integrations, CI/CD, scripts Org-scoped, explicit permission list, optional expiry
OAuth 2.1 access token Integrations acting on behalf of a user, MCP agents Token scopes ∩ the user’s role permissions

This tutorial uses an API key. Either way, the credential goes in the Authorization header as a bearer token. See the Authentication guide for details on both.

Terminal window
mkdir pidgr-integration && cd pidgr-integration
go mod init example.com/pidgr-integration
Terminal window
go get github.com/pidgr/pidgr-proto/gen/go@latest
go get google.golang.org/grpc
package main
import (
"context"
"crypto/tls"
"fmt"
"log"
"os"
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 := os.Getenv("PIDGR_API_KEY") // never hardcode keys
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.TemplateVariableSource_TEMPLATE_VARIABLE_SOURCE_PROFILE, Required: true},
{Name: "content", Source: pidgrv1.TemplateVariableSource_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.
// Every campaign needs a workflow — a DAG of steps that drives its
// lifecycle. CreateCampaign rejects requests without one.
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."}},
},
Workflow: &pidgrv1.WorkflowDefinition{
Steps: []*pidgrv1.WorkflowStep{
{
Id: "notify",
Type: pidgrv1.StepType_STEP_TYPE_SEND_NOTIFICATION,
Config: &pidgrv1.WorkflowStep_SendNotification{
SendNotification: &pidgrv1.SendNotificationConfig{
Type: "push",
ActionLabel: "Acknowledge",
ActionType: pidgrv1.ActionType_ACTION_TYPE_ACK,
},
},
},
},
},
})
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)
}
}
Terminal window
mkdir pidgr-integration && cd pidgr-integration
npm init -y
npm install typescript tsx @types/node --save-dev

The @pidgr/proto package is published on npm as a public package.

Terminal window
npm install @pidgr/proto @connectrpc/connect @connectrpc/connect-web @bufbuild/protobuf
import { createClient } from "@connectrpc/connect";
import { createConnectTransport } from "@connectrpc/connect-web";
import { CampaignService } from "@pidgr/proto/pidgr/v1/campaign_pb";
import {
TemplateService,
TemplateVariableSource,
} from "@pidgr/proto/pidgr/v1/template_pb";
import { ActionType, StepType } from "@pidgr/proto/pidgr/v1/common_pb";
// Create a transport with API key authentication
const 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 clients
const 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: TemplateVariableSource.PROFILE, required: true },
{ name: "content", source: TemplateVariableSource.CUSTOM, required: true },
],
});
console.log(`Created template: ${tmpl.template!.id} (version ${tmpl.template!.version})`);
// 2. Create a campaign using the template.
// Every campaign needs a workflow — CreateCampaign rejects requests without one.
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." } },
],
workflow: {
steps: [
{
id: "notify",
type: StepType.SEND_NOTIFICATION,
config: {
case: "sendNotification",
value: {
type: "push",
actionLabel: "Acknowledge",
actionType: ActionType.ACK,
},
},
},
],
},
});
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);
Terminal window
PIDGR_API_KEY=pidgr_k_your_key npx tsx main.ts

Because the API supports the Connect protocol, every RPC is also reachable as a plain HTTP POST with a JSON body — no client stubs required. The URL is /{package}.{Service}/{Method}:

Terminal window
curl https://api.pidgr.com/pidgr.v1.CampaignService/GetCampaign \
-H "Authorization: Bearer $PIDGR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"campaignId": "campaign-uuid"}'

Field names follow protobuf JSON conventions (camelCase). This is handy for quick exploration and for platforms without a gRPC client; for production integrations, prefer the generated stubs, which give you type safety and streaming support.

The pidgr-proto repository is open source (Apache 2.0) and contains the canonical Protocol Buffer definitions for all Pidgr services.

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 pidgr-proto (crates.io) cargo add pidgr-proto

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:

Terminal window
git clone https://github.com/pidgr/pidgr-proto.git
cd pidgr-proto
# Install buf (https://buf.build/docs/installation)
# Modify buf.gen.yaml for your target language
buf generate

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())
case codes.FailedPrecondition:
log.Printf("campaign not in a startable state: %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}`);
}
}
}

PermissionDenied errors name the missing permission — add it to the API key (or, for OAuth tokens, check both the granted scopes and the user’s role).

  • Never hardcode API keys — use environment variables or a secrets manager
  • Scope API keys — grant only the permissions your integration needs
  • Set an expiry or 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_at on API keys to detect unused or suspicious activity
  • Prefer OAuth for user-facing tools — tokens follow the user’s role and are individually revocable
  • 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