# Pidgely Docs > Pidgely is an email API for developers that delivers through its own MTA (pre-launch, early access). Base URL https://api.pidgely.com, official SDKs for Node.js (`pidgely`), Python, PHP, Go and Ruby (preview), SMTP relay (smtp.pidgely.com), signed webhooks (Standard Webhooks), templates, contacts, segments and broadcasts. Teams moving from Resend: see the migration guide. Every page below is plain Markdown (append `.md` to any docs URL). The OpenAPI 3.1 document is the source of truth for the HTTP API. - Base URL: https://api.pidgely.com - Auth: `Authorization: Bearer pdg__` - OpenAPI: https://docs.pidgely.com/openapi.yaml (also https://docs.pidgely.com/openapi.json) - Everything in one file: https://docs.pidgely.com/llms-full.txt - Product site: https://pidgely.com --- # Introduction > Pidgely is an email API for developers that delivers through its own MTA. Official SDKs for Node.js, Python, PHP, Go and Ruby, SMTP relay and signed webhooks. Source: https://docs.pidgely.com/ Pidgely is an email API for developers. Send transactional and broadcast email with the [official SDKs](https://docs.pidgely.com/sdks/) for Node.js, Python, PHP, Go and Ruby, with plain HTTP, or through SMTP. Already sending with another provider? The API follows the conventions you know, so [migrating](https://docs.pidgely.com/guides/migrate-from-resend/) is mostly a configuration change. Behind the API, Pidgely runs **its own MTA**. It signs DKIM per domain, aligns SPF through a return-path subdomain, separates transactional and broadcast traffic into their own IP pools, warms new IPs up gradually and processes bounces and complaints itself. > [!NOTE] > Pidgely is in **early access**. Accounts are being enabled in waves: [request access](https://pidgely.com/#waitlist). Everything on this site describes behaviour that exists today; features that are not available yet are marked as such. ## Start here - **[Quickstart](https://docs.pidgely.com/quickstart/)**: verify a domain and send your first email in a couple of minutes, in Node.js, Python, PHP, Go, Ruby or cURL. - **[SDKs](https://docs.pidgely.com/sdks/)**: install and configure the official SDK for your language. - **[Migrate from Resend](https://docs.pidgely.com/guides/migrate-from-resend/)**: move existing code and data over, plus the differences worth knowing. - **[API reference](https://docs.pidgely.com/api-reference/)**: every endpoint, generated from the [OpenAPI 3.1 document](https://docs.pidgely.com/openapi.yaml). ## What you can do | Capability | Where to read | |---|---| | Send transactional email: single, batch of up to 100, scheduled with natural language (`in 1 hour`), attachments from base64 or a URL | [Sending email](https://docs.pidgely.com/guides/sending/) | | Authenticate your domain with DKIM, an aligned return path (SPF) and DMARC | [Domains & DNS](https://docs.pidgely.com/guides/domains/) | | Store reusable templates with typed variables, drafts and published versions | [Templates](https://docs.pidgely.com/guides/templates/) | | Manage contacts and segments, send broadcasts with personalization and one-click unsubscribe | [Contacts](https://docs.pidgely.com/guides/contacts/), [Broadcasts](https://docs.pidgely.com/guides/broadcasts/) | | Receive signed webhooks for deliveries, bounces, complaints and contact changes | [Webhooks](https://docs.pidgely.com/guides/webhooks/) | | Send through SMTP from any framework (`smtp.pidgely.com`) | [SMTP relay](https://docs.pidgely.com/guides/smtp/) | | Keep bad addresses out automatically with the suppression list | [Suppressions](https://docs.pidgely.com/guides/suppressions/) | ## What is not available yet So that nothing misbehaves silently, these features are either rejected or documented as inert: - **Open and click tracking.** The flags are accepted and always reported as `false`; `email.opened` and `email.clicked` webhooks never fire. - **Inbound email** (receiving domains, inboxes, `email.received`). - **Automations**, topics, contact imports, logs and a few other endpoints listed in [Migrate from Resend](https://docs.pidgely.com/guides/migrate-from-resend/#not-supported-yet). ## Basics - **Base URL:** `https://api.pidgely.com` - **Authentication:** `Authorization: Bearer pdg__` ([Authentication](https://docs.pidgely.com/authentication/)) - **Format:** JSON request and response bodies, UTF-8 - **Errors:** `{"statusCode", "name", "message"}` ([Errors](https://docs.pidgely.com/errors/)) - **Lists:** cursor pagination with `limit`, `after` and `before` ([Pagination](https://docs.pidgely.com/pagination/)) ## For AI assistants and agents Every page of these docs is also available as plain Markdown: add `.md` to the URL (for example [/quickstart.md](https://docs.pidgely.com/quickstart.md)) or use the **Copy page as Markdown** button. [`/llms.txt`](https://docs.pidgely.com/llms.txt) indexes all pages and [`/llms-full.txt`](https://docs.pidgely.com/llms-full.txt) contains all of them in one file. The [MCP server](https://docs.pidgely.com/mcp/) (preview) lets agents send email and manage resources directly. --- # Quickstart > Create an API key, verify your domain and send your first email with Pidgely in about two minutes, with the official SDKs or cURL. Source: https://docs.pidgely.com/quickstart/ This guide sends a real email through Pidgely. You need an account (Pidgely is in [early access](https://pidgely.com/#waitlist)) and access to the DNS of a domain you own. ## 1. Create an API key Create a key in the dashboard. It looks like `pdg_k3x7m2qa4bnp6c5d_xyy4u7c2…` and is shown **only once**, so store it in your secret manager. For a first test a `full_access` key is simplest; in production, prefer a [`sending_access` key restricted to one domain](https://docs.pidgely.com/authentication/#domain-restricted-keys). ```bash export PIDGELY_API_KEY=pdg_your_key_here ``` ## 2. Add and verify your domain Pidgely only sends from domains you have verified. Add yours in the dashboard, or with the API: ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.domains.create({ name: 'acme.com', }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Domains.create({ "name": "acme.com", }) print(result) ``` ```php tab="PHP" domains->create([ 'name' => 'acme.com', ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Domains.Create(ctx, &pidgely.CreateDomainRequest{ Name: "acme.com", }) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Domains.create({ name: "acme.com" }) puts result ``` ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/domains' \ -H "Authorization: Bearer $PIDGELY_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "name": "acme.com" }' ``` The response lists the DNS records to publish: a DKIM `TXT` record, an `MX` and an SPF `TXT` record on `bounces.acme.com` (the return path), and an optional DMARC record. Add them at your DNS provider, then ask Pidgely to check: ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.domains.verify('0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51'); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Domains.verify("0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51") print(result) ``` ```php tab="PHP" domains->verify('0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51'); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Domains.Verify(ctx, "0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51") if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Domains.verify("0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51") puts result ``` ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/domains/0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51/verify' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` New domains are also re-checked automatically every few minutes. [Domains & DNS](https://docs.pidgely.com/guides/domains/) explains each record and how to troubleshoot. ## 3. Install an SDK Install the official Pidgely SDK for your language. The cURL tab needs nothing. ```bash tab="Node.js" npm install pidgely ``` ```bash tab="Python" pip install pidgely ``` ```bash tab="PHP" # Guzzle is the suggested HTTP client; any PSR-18 client works composer require pidgely/pidgely-php guzzlehttp/guzzle ``` ```bash tab="Go" go get github.com/pidgely/pidgely-go ``` ```bash tab="Ruby" gem install pidgely ``` ```bash tab="cURL" # nothing to install ``` > [!PREVIEW] > The Go SDK is published (v0.1). The Node.js, Python, PHP and Ruby SDKs are in preview (0.1) and being published now: see [SDKs](https://docs.pidgely.com/sdks/) for links and status. ## 4. Send an email Replace `acme.com` with your verified domain and `delivered@example.com` with an inbox you can check. ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.emails.send({ from: 'Acme ', to: ['delivered@example.com'], subject: 'Hello from Pidgely', html: '

It works! 🎉

', }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] email = pidgely.Emails.send({ "from": "Acme ", "to": ["delivered@example.com"], "subject": "Hello from Pidgely", "html": "

It works! 🎉

", }) print(email) ``` ```php tab="PHP" emails->send([ 'from' => 'Acme ', 'to' => ['delivered@example.com'], 'subject' => 'Hello from Pidgely', 'html' => '

It works! 🎉

', ]); var_dump($email); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) sent, err := client.Emails.Send(ctx, &pidgely.SendEmailRequest{ From: "Acme ", To: []string{"delivered@example.com"}, Subject: "Hello from Pidgely", Html: "

It works! 🎉

", }) if err != nil { panic(err) } fmt.Println(sent) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] email = Pidgely::Emails.send({ from: "Acme ", to: ["delivered@example.com"], subject: "Hello from Pidgely", html: "

It works! 🎉

" }) puts email ``` ```bash tab="cURL" curl -X POST https://api.pidgely.com/emails \ -H "Authorization: Bearer $PIDGELY_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "from": "Acme ", "to": ["delivered@example.com"], "subject": "Hello from Pidgely", "html": "

It works! 🎉

" }' ``` The response contains the email id: ```json { "id": "0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21" } ``` ## 5. Check what happened Retrieve the email to see its `last_event` (`queued`, `delivered`, `bounced`, …): ```js const { data } = await pidgely.emails.get('0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21'); console.log(data.last_event); ``` For production, don't poll: subscribe to [webhooks](https://docs.pidgely.com/guides/webhooks/) such as `email.delivered`, `email.bounced` and `email.complained`. ## Next steps - [Sending email](https://docs.pidgely.com/guides/sending/): batch, scheduling, attachments, tags and headers. - [Idempotency](https://docs.pidgely.com/idempotency/): retry sends safely with an `Idempotency-Key`. - [Webhooks](https://docs.pidgely.com/guides/webhooks/): verify signatures and handle retries. - [Migrate from Resend](https://docs.pidgely.com/guides/migrate-from-resend/): moving existing code over. - [SMTP relay](https://docs.pidgely.com/guides/smtp/): send from frameworks that only speak SMTP. --- # Authentication > Authenticate with API keys sent as Bearer tokens. Choose full_access or sending_access, and restrict sending keys to a single domain. Source: https://docs.pidgely.com/authentication/ Every request to the API carries an API key in the `Authorization` header: ```http POST /emails HTTP/1.1 Host: api.pidgely.com Authorization: Bearer pdg_k3x7m2qa4bnp6c5d_xyy4u7c2r3w5t6v7m8n9p2q3r4s5t6u7 Content-Type: application/json ``` The scheme must be exactly `Bearer ` (capital B, one space). A request without it gets `401 missing_api_key`; an unknown or revoked key gets `403 invalid_api_key`. ## Key format Keys look like `pdg__`: a public prefix that identifies the key and a secret part. Pidgely stores only an HMAC of the secret, so the full key is shown **once**, when you create it. If you lose it, create a new key and revoke the old one. ## Permissions | Permission | Can call | Use it for | |---|---|---| | `full_access` (default) | Every endpoint | Back-office scripts, infrastructure as code, the [MCP server](https://docs.pidgely.com/mcp/) when you trust the agent | | `sending_access` | `POST /emails`, `POST /emails/batch` and `GET /emails/{id}` only | Application servers that only send | A `sending_access` key that calls any other endpoint gets: ```json { "statusCode": 401, "name": "restricted_api_key", "message": "This API key is restricted to only send emails." } ``` The same applies to [SMTP](https://docs.pidgely.com/guides/smtp/): any valid key can submit mail there. ## Domain-restricted keys Pass `domain_id` when you [create a key](https://docs.pidgely.com/api-reference/api-keys/create/) to restrict it to one verified domain. Such a key is always `sending_access`, and sending from any other domain fails with `403 validation_error` (`this API key cannot send from `). ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.apiKeys.create({ name: 'Web app (acme.com)', permission: 'sending_access', domainId: '0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51', }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.ApiKeys.create({ "name": "Web app (acme.com)", "permission": "sending_access", "domain_id": "0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51", }) print(result) ``` ```php tab="PHP" apiKeys->create([ 'name' => 'Web app (acme.com)', 'permission' => 'sending_access', 'domain_id' => '0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51', ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.ApiKeys.Create(ctx, &pidgely.CreateApiKeyRequest{ Name: "Web app (acme.com)", Permission: "sending_access", DomainId: "0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51", }) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::ApiKeys.create({ name: "Web app (acme.com)", permission: "sending_access", domain_id: "0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51" }) puts result ``` ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/api-keys' \ -H "Authorization: Bearer $PIDGELY_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "name": "Web app (acme.com)", "permission": "sending_access", "domain_id": "0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51" }' ``` Give each service its own restricted key: a leaked key can then only send from one domain and can't read your contacts, change DNS settings or create new keys. ## Keep keys on the server The API does not answer CORS requests, so it can't be called from a browser, and it shouldn't be: anyone could read a key embedded in front-end code. Call Pidgely from your backend, a serverless function or a job runner, and load the key from an environment variable or a secret manager. ## Rotating and revoking keys 1. [Create](https://docs.pidgely.com/api-reference/api-keys/create/) a new key and deploy it. 2. [Delete](https://docs.pidgely.com/api-reference/api-keys/delete/) the old key. Revocation takes effect within about 30 seconds, as authentication results are cached briefly. `GET /api-keys` shows `last_used_at` for each key, which helps to spot keys that are no longer used. ## Accounts and limits All keys of an account share the same [rate limit and quotas](https://docs.pidgely.com/rate-limits/), suppression list, domains and contacts. New accounts start in a sandbox on the Free plan; see [Rate limits & quotas](https://docs.pidgely.com/rate-limits/#plans) for what that means. --- # Errors > Error responses have the shape {statusCode, name, message}. The full list of error names, their HTTP status and how to handle them. Source: https://docs.pidgely.com/errors/ Pidgely uses conventional HTTP status codes: `2xx` for success, `4xx` for a problem with the request, `5xx` for a problem on our side. Errors have this body: ```json { "statusCode": 422, "name": "validation_error", "message": "to: at least one recipient is required" } ``` - `name` is stable and machine-readable: branch on it, never on `message`. - `message` is meant for humans. For validation errors it starts with the field, for example `subject: is too long` or `[3].to: …` inside a batch. - Every response carries an `X-Request-Id` header. Send your own (up to 64 characters) to correlate logs, and include it when you [contact support](https://docs.pidgely.com/support/). ## Error names | Status | `name` | When | What to do | |---|---|---|---| | 400 | `invalid_idempotency_key` | `Idempotency-Key` longer than 256 characters | Use a shorter key | | 401 | `missing_api_key` | No `Authorization: Bearer …` header | Send the key | | 401 | `restricted_api_key` | A `sending_access` key called an endpoint that needs `full_access` | Use a full-access key for that call | | 403 | `invalid_api_key` | The key does not exist or was revoked | Check the key and the environment | | 403 | `validation_error` | The request is valid but not allowed: `from` domain not verified, key restricted to another domain, plan limit on domains reached, domain already added or verified by another account, sending paused or account suspended | Read `message`; verify the domain or fix the account state | | 404 | `not_found` | The resource does not exist in your account (also returned for ids that are not valid UUIDs) | Check the id | | 409 | `invalid_idempotent_request` | The `Idempotency-Key` was used before with a different request | Use a new key for a new request | | 409 | `not_cancelable`, `not_reschedulable` | The email is no longer scheduled (already sent or canceled) | Nothing to cancel or update | | 409 | `alias_taken` | Another template already uses this alias | Choose another alias | | 409 | `broadcast_not_draft`, `broadcast_not_cancelable`, `broadcast_in_progress` | The broadcast is in a state that does not allow the operation | See [Broadcasts](https://docs.pidgely.com/guides/broadcasts/#lifecycle) | | 422 | `validation_error` | A field is missing or invalid, the JSON is malformed, or the body is too large | Fix the request; `message` names the field | | 422 | `invalid_attachment` | An attachment has neither `content` nor `path` | Provide one of them | | 429 | `rate_limit_exceeded` | Too many requests per second | Wait `Retry-After` seconds and retry | | 429 | `daily_quota_exceeded` | The daily quota (UTC day) is used up | Wait for the next UTC day or upgrade | | 429 | `monthly_quota_exceeded` | The monthly quota is used up | Wait for the next month or upgrade | | 500 | `application_error` | Unexpected error on our side | Retry with backoff (with the same `Idempotency-Key` for sends) | ## Retrying - **Retry** `429` and `5xx` with exponential backoff. For `429 rate_limit_exceeded`, honour `Retry-After`. - **Don't retry** other `4xx` errors unchanged: they fail the same way every time. - **Make sends safe to retry** with an [`Idempotency-Key`](https://docs.pidgely.com/idempotency/): a network timeout then never turns into a duplicate email. Responses with `5xx` are not stored, so a retry with the same key runs again. ## Native API errors The native `/v1` API uses a different shape, with the field that failed and the request id: ```json { "error": { "code": "validation_error", "message": "at least one recipient is required", "field": "to", "request_id": "8f2c1a7d9e3b4c60" } } ``` Codes there are the internal ones (`domain_not_verified`, `tenant_paused`, `plan_limit`, `idempotency_key_reused`, `rate_limited`, `insufficient_scope`, …), with the same statuses: 401 missing key, 403 forbidden, 404 not found, 409 conflict, 422 validation, 429 rate or quota. The [main API](https://docs.pidgely.com/api-reference/) is the recommended surface for new code. ## Request limits | Limit | Value | |---|---| | `POST /emails` and `POST /emails/batch` body | 50 MiB | | `POST`/`PATCH` on templates and broadcasts | 5 MiB | | Other write endpoints | 1 MiB | | Built message (headers, bodies and attachments, after encoding) | 40 MiB | | Request timeout | 60 seconds | A body over its limit gets `422 validation_error` with `request body too large`. --- # Rate limits & quotas > The API allows 10 requests per second per account by default. Plans set daily and monthly sending quotas; deliveries are paced per account. Source: https://docs.pidgely.com/rate-limits/ Three different limits apply to an account. They protect the platform and your sender reputation, and each one fails in its own, recognizable way. | Limit | Applies to | When exceeded | |---|---|---| | Request rate | API requests per second, per account | `429 rate_limit_exceeded` with `Retry-After` | | Sending quotas | Recipients accepted per UTC day and per month | `429 daily_quota_exceeded` / `monthly_quota_exceeded` | | Delivery pacing | How fast accepted mail leaves the MTA | Nothing fails: deliveries are spread over time | ## Request rate The default is **10 requests per second per account**, with bursts up to twice that. The limit is shared by every API key and dashboard session of the account. When you exceed it: ```http HTTP/1.1 429 Too Many Requests Retry-After: 1 Content-Type: application/json {"statusCode":429,"name":"rate_limit_exceeded","message":"Too many requests. Please limit the number of requests per second."} ``` Wait for the number of seconds in `Retry-After`, then retry. There are no `ratelimit-*` headers. To send many emails, use [`POST /emails/batch`](https://docs.pidgely.com/api-reference/emails/send-batch/): one request carries up to 100 emails, so 10 requests per second can submit up to 1,000 emails per second. If you need a higher request rate, [contact support](https://docs.pidgely.com/support/). ## Sending quotas Quotas count **accepted recipients**: an email to three recipients uses three. Recipients that are on your [suppression list](https://docs.pidgely.com/guides/suppressions/) are not delivered and do not count. - The **daily** quota resets at 00:00 UTC. - The **monthly** quota follows the UTC calendar month. - A [batch](https://docs.pidgely.com/api-reference/emails/send-batch/) that would go over the daily quota is rejected as a whole. - Quota errors do not include `Retry-After`. Broadcasts that hit a quota pause and resume automatically the next UTC day ([Broadcasts](https://docs.pidgely.com/guides/broadcasts/#quotas)). ### Plans | Plan | Emails per month | Daily cap | Sending domains | |---|---|---|---| | Free | 10,000 | 500 | 1 | | Starter | 50,000 | — | 3 | | Growth | 250,000 | — | 10 | | Scale | 1,000,000 | — | Unlimited | Prices and overage are on [pidgely.com/#pricing](https://pidgely.com/#pricing). New accounts start in a **sandbox** on the Free plan: the sandbox applies the Free plan's quotas and domain limit, with no other restriction on what you can send or to whom. ## Delivery pacing Accepting an email and delivering it are separate steps. Each account delivers at its own contracted rate: a burst from one customer is spread over time instead of starving everyone else, and receiving servers see a steady stream. You don't need to throttle on your side: send as fast as the request rate allows and Pidgely schedules the deliveries. ## Reputation guard To protect your domain and the shared IPs, sending is **paused automatically** when, over the last 7 days: - the bounce rate is above **4%** (evaluated once at least 200 messages were delivered or bounced), or - the complaint rate is above **0.1%** (evaluated once at least 1,000 messages were delivered). While paused, new sends get `403 validation_error` (`sending is paused for this account: …`) and queued messages wait. Clean your list (see [Deliverability](https://docs.pidgely.com/guides/deliverability/)) and [contact support](https://docs.pidgely.com/support/) to resume. --- # Idempotency > Send an Idempotency-Key with POST /emails and /emails/batch to retry safely. The response is replayed for 24 hours; a different body with the same key gets 409. Source: https://docs.pidgely.com/idempotency/ Networks fail. If a send times out you can't know whether Pidgely received it, and retrying blindly can deliver the same email twice. An **idempotency key** makes the retry safe: Pidgely stores the first response and replays it for every retry with the same key. ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.emails.send({ from: 'Acme ', to: ['steve@example.com'], subject: 'Order confirmed', html: '

Thanks!

', }, { idempotencyKey: 'order-1234/confirmation', }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Emails.send({ "from": "Acme ", "to": ["steve@example.com"], "subject": "Order confirmed", "html": "

Thanks!

", }, { "idempotency_key": "order-1234/confirmation", }) print(result) ``` ```php tab="PHP" emails->send([ 'from' => 'Acme ', 'to' => ['steve@example.com'], 'subject' => 'Order confirmed', 'html' => '

Thanks!

', ], [ 'idempotency_key' => 'order-1234/confirmation', ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Emails.Send(ctx, &pidgely.SendEmailRequest{ From: "Acme ", To: []string{"steve@example.com"}, Subject: "Order confirmed", Html: "

Thanks!

", }, pidgely.WithIdempotencyKey("order-1234/confirmation")) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Emails.send({ from: "Acme ", to: ["steve@example.com"], subject: "Order confirmed", html: "

Thanks!

" }, { idempotency_key: "order-1234/confirmation" }) puts result ``` ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/emails' \ -H "Authorization: Bearer $PIDGELY_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: order-1234/confirmation' \ -d '{ "from": "Acme ", "to": [ "steve@example.com" ], "subject": "Order confirmed", "html": "

Thanks!

" }' ``` With the Node.js SDK, pass it as an option: `pidgely.emails.send(params, { idempotencyKey: 'order-1234/confirmation' })`. The SDK then also retries failed sends automatically. ## How it works | | Behaviour | |---|---| | Endpoints | `POST /emails`, `POST /emails/batch` (and `/v1/emails`, `/v1/emails/batch`). Over [SMTP](https://docs.pidgely.com/guides/smtp/#special-headers), use the `Pidgely-Idempotency-Key` header. | | Key | 1 to 256 characters; longer keys get `400 invalid_idempotency_key`. An empty header means no idempotency. | | Scope | Per account: two API keys of the same account share the key space. | | Lifetime | 24 hours from the first request. | | Same key, same request | The stored status code and body are returned, with the header `Idempotent-Replayed: true`. Nothing is sent again. | | Same key, different request | `409 invalid_idempotent_request` (`Idempotency-Key was used with a different request`). The request is compared by method, path and exact body. | | Errors | `4xx` responses are stored and replayed like successes. `5xx` responses are not stored, so a retry runs the request again. | ## Choosing keys Derive the key from the business event that triggers the email, not from the attempt: - `user-42/welcome`, `order-1234/confirmation`, `invoice-2026-10/acme`: retries of the same event reuse the key, a different event gets a different key. - A random UUID generated **once per event** and stored with it works too. A new UUID per retry defeats the purpose. > [!WARNING] > Two requests with the same new key that arrive at exactly the same time are not serialized: both can run. Retry after a timeout or an error, not in parallel. ## Retry recipe 1. Send with an idempotency key. 2. On a timeout, a connection error, `429` or `5xx`, retry with the **same key and the same body**, with exponential backoff. 3. A replayed success has `Idempotent-Replayed: true` and the original email id. 4. Don't retry other `4xx` errors unchanged; see [Errors](https://docs.pidgely.com/errors/#retrying). --- # Pagination > List endpoints use cursor pagination with limit, after and before, and return {object, has_more, data}, newest first. Source: https://docs.pidgely.com/pagination/ Every list endpoint (`GET /emails`, `GET /domains`, `GET /contacts`, …) returns the same envelope: ```json { "object": "list", "has_more": true, "data": [ { "id": "0199d8a2-7a10-7d02-8b4e-51c2a9f0e7b3", "…": "…" }, { "id": "0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21", "…": "…" } ] } ``` Items are always ordered **newest first**. Ids are time-ordered (UUIDv7), so a cursor is simply the id of an item. ## Parameters | Parameter | Description | |---|---| | `limit` | Page size, from 1 to 100. Outside that range: `422 validation_error` (`The pagination limit must be a number between 1 and 100.`) | | `after` | Id of the **last** item of the previous page. Returns the next, older items. | | `before` | Id of the **first** item of the current page. Returns the previous, newer items. | `after` and `before` can't be combined (`You can only use either after or before, not both.`), and must be valid ids. When you omit `limit`, `GET /emails` and `GET /templates` return 20 items; the other lists return up to 1,000 items in one page. `has_more` tells you whether there is another page in the direction you are reading. ## Iterating over everything ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); let after; do { const { data: page, error } = await pidgely.contacts.list({ limit: 100, after }); if (error) throw new Error(error.message); for (const contact of page.data) console.log(contact.email); after = page.has_more ? page.data.at(-1).id : undefined; } while (after); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] after = None while True: page = pidgely.Contacts.list({"limit": 100, **({"after": after} if after else {})}) for contact in page["data"]: print(contact["email"]) if not page["has_more"]: break after = page["data"][-1]["id"] ``` ```bash tab="cURL" curl "https://api.pidgely.com/contacts?limit=100" -H "Authorization: Bearer $PIDGELY_API_KEY" # then, with the id of the last item: curl "https://api.pidgely.com/contacts?limit=100&after=0199d7f4-5d7b-7a2c-b4e1-9f3c2a6d8e05" -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Consistency List endpoints may be served by a read replica and lag a moment behind writes. Retrieving a single object (for example `GET /emails/{id}`) always reads the primary database, so an email is visible right after you send it. --- # Domains & DNS > Add a sending domain and publish its DKIM, return-path (MX + SPF) and DMARC records. How verification works and how to fix the usual DNS problems. Source: https://docs.pidgely.com/guides/domains/ Pidgely sends only from domains you have verified. Verification proves you control the domain and, more importantly, sets up the authentication that mailbox providers check before they trust your mail: **DKIM**, an **SPF**-aligned return path and **DMARC**. ## Add a domain ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.domains.create({ name: 'acme.com', }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Domains.create({ "name": "acme.com", }) print(result) ``` ```php tab="PHP" domains->create([ 'name' => 'acme.com', ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Domains.Create(ctx, &pidgely.CreateDomainRequest{ Name: "acme.com", }) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Domains.create({ name: "acme.com" }) puts result ``` ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/domains' \ -H "Authorization: Bearer $PIDGELY_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "name": "acme.com" }' ``` The response ([Create a domain](https://docs.pidgely.com/api-reference/domains/create/)) includes the records to publish. Names are **relative to your domain**: | Purpose | Type | Name | Value | |---|---|---|---| | DKIM | `TXT` | `pdg202610._domainkey` | `v=DKIM1; k=rsa; p=MIIBIjANBg…` (RSA-2048 public key) | | Return path | `MX` | `bounces` | `mta.pidgely.com`, priority `10` | | SPF | `TXT` | `bounces` | `v=spf1 include:_spf.mta.pidgely.com ~all` | | DMARC (recommended) | `TXT` | `_dmarc` | `v=DMARC1; p=none;` | Always copy the exact values from the API response or the dashboard: the DKIM selector (`pdg` plus the year and month the domain was added) and the key are unique to your domain. > [!TIP] > SPF lives on the **return-path subdomain** (`bounces.acme.com`), not on `acme.com`. You don't need to touch the SPF record of your root domain, and you can't exceed its 10-lookup limit because of Pidgely. ### Why these records - **DKIM** signs every message with a key only Pidgely holds; receivers fetch the public key from `pdg…._domainkey.acme.com`. Pidgely signs with relaxed/relaxed canonicalization and oversigns From, Subject and To. - **Return path.** Bounces go to `bounces.acme.com` (the envelope sender, with a per-message VERP address). The `MX` routes bounces back to Pidgely so they are [processed automatically](https://docs.pidgely.com/guides/suppressions/), and the SPF record authorizes Pidgely's servers for that subdomain. Because it is a subdomain of yours, SPF **aligns** with your From domain for DMARC. - **DMARC** tells receivers what to do when authentication fails and where to send reports. Start with `p=none`, watch the reports, then move to `quarantine` or `reject`. Pidgely does not require DMARC to send, but Gmail and Yahoo require it for bulk senders. Read more in [SPF, DKIM and DMARC explained](https://pidgely.com/blog/spf-dkim-dmarc-explained/). ## Options | Field | Description | |---|---| | `custom_return_path` | Label of the return-path subdomain, default `bounces`. One DNS label: lowercase letters, digits and hyphens (`^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$`). | | `tls` | `opportunistic` (default): use TLS whenever the receiving server offers it. `enforced`: deliver only over TLS, otherwise retry and eventually fail. Change it later with [Update a domain](https://docs.pidgely.com/api-reference/domains/update/). | `region` is informative, and open/click tracking and receiving are [not supported yet](https://docs.pidgely.com/guides/migrate-from-resend/#not-supported-yet): `capabilities.receiving: "enabled"` is rejected. ## Verification After publishing the records, call [Verify a domain](https://docs.pidgely.com/api-reference/domains/verify/) or wait: new domains are re-checked **every 5 minutes during their first 3 days**, and every domain is re-checked **daily** afterwards. | Status | Meaning | |---|---| | `not_started` | The domain was added but never checked. | | `pending` | Checked, but some required records are missing or wrong. DNS can take minutes (rarely hours) to propagate. | | `verified` | DKIM, the return-path `MX` and SPF all pass. You can send. | | `failed` | Records that used to pass no longer do (DNS changed), or the domain stayed unverified for more than 72 hours when verification was requested. Fix the DNS and verify again. | Each record in `records[]` has its own `status`, so you can see exactly which one is missing. Status changes emit a [`domain.updated` webhook](https://docs.pidgely.com/guides/webhooks/#events). ## Rules - **Exact match.** Mail from `news.acme.com` needs `news.acme.com` added as its own domain; `acme.com` doesn't cover subdomains. - **One account per domain.** A domain can be verified by only one Pidgely account. Verifying a domain already verified elsewhere fails with `… is verified by another account`. - **Plan limits.** Free allows 1 domain, Starter 3, Growth 10, Scale unlimited ([plans](https://docs.pidgely.com/rate-limits/#plans)). Adding more returns `403 validation_error`. - **Deleting a domain** stops sending from it immediately; messages still queued for it are retried and eventually fail. ## Troubleshooting **The record exists but Pidgely says it's missing.** Many DNS providers append your domain automatically. If you entered `pdg202610._domainkey.acme.com` as the name, the provider may have created `pdg202610._domainkey.acme.com.acme.com`. Enter only the relative part (`pdg202610._domainkey`). **Check what the world sees** with `dig` (or `nslookup -type=TXT`): ```bash dig +short TXT pdg202610._domainkey.acme.com dig +short MX bounces.acme.com dig +short TXT bounces.acme.com dig +short TXT _dmarc.acme.com ``` **DKIM value split or truncated.** A 2048-bit key is longer than 255 characters. Most providers split long TXT values automatically; if yours asks for it, paste the value as several quoted strings in one record (`"v=DKIM1; k=rsa; p=MIIB…" "…rest"`). Make sure no quotes or spaces were added inside the key. **MX on `bounces` conflicts with a CNAME.** A name can't have both. Remove the CNAME or choose another label with `custom_return_path`. **Two SPF records on the same name.** If `bounces.acme.com` already had an SPF record, merge them into one: a name can have only one `v=spf1` record. **Cloudflare and other proxies.** `MX` and `TXT` records are never proxied, so no special setting is needed. **Still stuck?** [Contact support](https://docs.pidgely.com/support/) with the domain name; we can see which check fails. --- # Sending email > Everything about POST /emails - recipients, HTML and text, batch sends, scheduling with natural language, attachments (base64 or remote path), tags, headers and streams. Source: https://docs.pidgely.com/guides/sending/ [`POST /emails`](https://docs.pidgely.com/api-reference/emails/send/) accepts one email; [`POST /emails/batch`](https://docs.pidgely.com/api-reference/emails/send-batch/) accepts up to 100. Both return as soon as the email is safely queued, with its id. Delivery, retries and bounce handling happen in the background and are reported through [webhooks](https://docs.pidgely.com/guides/webhooks/). ```json { "from": "Acme ", "to": ["steve@example.com"], "cc": ["billing@acme.com"], "reply_to": "support@acme.com", "subject": "Your invoice for October", "html": "

Hi Steve, your invoice is attached.

", "attachments": [{ "path": "https://files.acme.com/invoices/2026-10.pdf" }], "tags": [{ "name": "category", "value": "invoice" }] } ``` ## Sender and recipients - **`from`** is `addr@domain` or `Name `. The domain must be [verified](https://docs.pidgely.com/guides/domains/) in your account (exact match) and allowed for the API key. - **`to`, `cc`, `bcc`** take one address or a list. Up to **50 unique recipients** across the three; addresses are de-duplicated (case-insensitive local part, IDN domains normalized). - **`reply_to`** takes one address or a list and doesn't count toward the 50. - Addresses with an IP literal as domain, local parts over 64 characters or addresses over 254 characters are rejected. Each recipient is delivered separately and gets its own status. Recipients on your [suppression list](https://docs.pidgely.com/guides/suppressions/) are accepted but not delivered: they end as `suppressed`, don't count toward your quota, and trigger `email.suppressed`. ## Content - **`subject`** is required (up to 998 bytes) unless a [template](https://docs.pidgely.com/guides/templates/) provides it. - **`html`** and/or **`text`**: at least one is required. When you send only `html`, Pidgely generates the plain-text part for you. - The `Message-ID` uses your domain; lines always end in CRLF. ## Batch sending ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.batch.send([ { from: 'Acme ', to: ['alice@example.com'], subject: 'Hi Alice', html: '

Hi!

', }, { from: 'Acme ', to: ['bob@example'], subject: 'Hi Bob', html: '

Hi!

', }, ], { batchValidation: 'permissive', }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Batch.send([ { "from": "Acme ", "to": ["alice@example.com"], "subject": "Hi Alice", "html": "

Hi!

", }, { "from": "Acme ", "to": ["bob@example"], "subject": "Hi Bob", "html": "

Hi!

", }, ], { "batch_validation": "permissive", }) print(result) ``` ```php tab="PHP" batch->send([ [ 'from' => 'Acme ', 'to' => ['alice@example.com'], 'subject' => 'Hi Alice', 'html' => '

Hi!

', ], [ 'from' => 'Acme ', 'to' => ['bob@example'], 'subject' => 'Hi Bob', 'html' => '

Hi!

', ], ], [ 'batch_validation' => 'permissive', ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Batch.Send(ctx, []pidgely.SendEmailRequest{ { From: "Acme ", To: []string{"alice@example.com"}, Subject: "Hi Alice", Html: "

Hi!

", }, { From: "Acme ", To: []string{"bob@example"}, Subject: "Hi Bob", Html: "

Hi!

", }, }, pidgely.WithBatchValidation(pidgely.BatchPermissive)) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Batch.send([ { from: "Acme ", to: ["alice@example.com"], subject: "Hi Alice", html: "

Hi!

" }, { from: "Acme ", to: ["bob@example"], subject: "Hi Bob", html: "

Hi!

" } ], { batch_validation: "permissive" }) puts result ``` ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/emails/batch' \ -H "Authorization: Bearer $PIDGELY_API_KEY" \ -H 'Content-Type: application/json' \ -H 'x-batch-validation: permissive' \ -d '[ { "from": "Acme ", "to": [ "alice@example.com" ], "subject": "Hi Alice", "html": "

Hi!

" }, { "from": "Acme ", "to": [ "bob@example" ], "subject": "Hi Bob", "html": "

Hi!

" } ]' ``` - 1 to 100 emails per request, each with the full single-send shape (attachments, `scheduled_at` and `template` included). - **`strict`** (default): all or nothing. The first invalid email fails the request, with its index in the message (`[1].to: …`). - **`permissive`**: valid emails are queued and invalid ones are listed in `errors`: ```json { "data": [{ "id": "0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21" }], "errors": [{ "index": 1, "message": "to: invalid address \"bob@example\": invalid domain \"example\"" }] } ``` A batch that would exceed the daily quota is rejected as a whole. Batches support [idempotency keys](https://docs.pidgely.com/idempotency/) like single sends. ## Scheduling Set `scheduled_at` to send later, up to **30 days** ahead. It accepts ISO 8601: ```json { "scheduled_at": "2026-10-20T09:00:00-03:00" } ``` or natural language, evaluated in **UTC**: | You write | Means | |---|---| | `in 10 minutes`, `in 2 hours`, `in 3 days`, `in 1 week` | Relative to now. Units: `s`/`sec`/`seconds`, `m`/`min`/`minutes`, `h`/`hr`/`hours`, `d`/`days`, `w`/`weeks` | | `in an hour`, `in a day`, `one week from now`, `3 days from now` | `a`, `an` and `one` work as numbers; `in` and `from now` are optional | | `tomorrow`, `tomorrow at 9am`, `tomorrow 18:30`, `today at 5:45pm` | A time of day in UTC (12- or 24-hour) | | `now`, `immediately`, `right now` | Send now | Matching ignores case and extra spaces. ISO dates without an offset are treated as UTC. A time in the past (or now) sends immediately; anything unrecognised fails with `unrecognised date "…": use ISO 8601 or phrases like "in 1 hour"`. While the email is still scheduled you can: - **reschedule** it with [`PATCH /emails/{id}`](https://docs.pidgely.com/api-reference/emails/update/) and a new `scheduled_at` (in the future, within 30 days); - **cancel** it with [`POST /emails/{id}/cancel`](https://docs.pidgely.com/api-reference/emails/cancel/). Both return `409` (`not_reschedulable` / `not_cancelable`) once the email has been sent or canceled. A scheduled email shows `last_event: "scheduled"`. ## Attachments Each attachment has **either** `content` (standard base64) **or** `path` (an `http`/`https` URL that Pidgely downloads when you send): ```json { "attachments": [ { "filename": "invoice.pdf", "content": "JVBERi0xLjcKJcfsj6IKNSAwIG9iago8PC9MZW5ndGggNiAwIFIvRmlsdGVyIC9GbGF0ZURlY29kZT4+CnN0cmVhbQp4nO1d…" }, { "path": "https://files.acme.com/reports/october.csv", "content_type": "text/csv" }, { "filename": "logo.png", "content": "iVBORw0KGgoAAAANSUhEUgAA…", "content_id": "logo" } ], "html": "\"Acme\"

Your files are attached.

" } ``` - **Size.** All attachments of an email share a budget of **40 MiB** (decoded), and the whole message must stay under 40 MiB after encoding. There is no separate limit on the number of attachments. - **Remote `path`.** Fetched once at send time with a 30-second timeout. Only a `200` response is accepted, redirects are not followed, URLs with credentials and private or internal addresses are refused. The filename defaults to the last segment of the URL path, and the response `Content-Type` is used unless it is `text/html`. - **Inline images.** Set `content_id` and reference it as `cid:` in the HTML. - **Content type** is guessed from the extension when you omit `content_type` (fallback `application/octet-stream`). - **Blocked types.** Executables and scripts are rejected: `.exe .bat .cmd .com .cpl .dll .js .jse .msi .msp .pif .scr .vbs .vbe .wsf .wsh .hta .jar .ps1 .lnk .iso .img .vhd .reg .app .apk`. - File names can't contain `/`, `\` or line breaks. Attachments of a sent email can be listed and downloaded with [signed links](https://docs.pidgely.com/api-reference/emails/list-attachments/) valid for one hour. ## Tags Tags are name/value pairs (`^[A-Za-z0-9_-]{1,256}$` for both) stored with the email and included in its webhook events. Use them to tie an email back to your own data: ```json { "tags": [{ "name": "user_id", "value": "42" }, { "name": "category", "value": "password_reset" }] } ``` ## Custom headers `headers` adds headers to the message: ```json { "headers": { "X-Entity-Ref-ID": "order-1234", "List-Id": "Acme receipts " } } ``` Header names are 1 to 76 printable ASCII characters without `:`; values can't contain line breaks. Headers the platform manages are rejected: `From`, `To`, `Cc`, `Bcc`, `Subject`, `Date`, `Message-ID`, `MIME-Version`, `Content-Type`, `Content-Transfer-Encoding`, `DKIM-Signature`, `Return-Path`, `Received`, `Sender`, `Reply-To` (use `reply_to`), `Feedback-ID`, `X-Pidgely-Ref`, `Authentication-Results` and the `ARC-*` headers. ## Streams Pidgely keeps **transactional** and **broadcast** mail apart, on separate IP pools, so a marketing campaign never hurts the delivery of password resets. `POST /emails` uses the transactional stream by default. Set `"stream": "broadcast"` for bulk or marketing mail sent through the emails endpoint: those messages get `List-Unsubscribe` and `List-Unsubscribe-Post` headers ([RFC 8058](https://pidgely.com/blog/one-click-unsubscribe-rfc-8058/) one-click unsubscribe). [Broadcasts](https://docs.pidgely.com/guides/broadcasts/) always use the broadcast stream. ## Templates Instead of `html`/`text`, reference a published [template](https://docs.pidgely.com/guides/templates/) and pass its variables: ```json { "to": ["steve@example.com"], "template": { "id": "order-confirmation", "variables": { "ORDER_ID": "1234", "TOTAL": 59.9 } } } ``` ## Status of an email [`GET /emails/{id}`](https://docs.pidgely.com/api-reference/emails/retrieve/) returns `last_event`, the most advanced status across the recipients: | `last_event` | Meaning | |---|---| | `queued` | Accepted, waiting for delivery | | `scheduled` | Waiting for its `scheduled_at` | | `delivery_delayed` | A temporary failure; Pidgely retries for up to 72 hours | | `delivered` | The receiving server accepted the message | | `bounced` | Permanently rejected; the address is now [suppressed](https://docs.pidgely.com/guides/suppressions/) | | `complained` | The recipient marked it as spam | | `suppressed` | Not sent: the address is on the suppression list | | `failed` | Gave up (retry window exhausted, account suspended, content expired) | | `canceled` | Canceled before sending | The bodies of sent emails are kept for 30 days. ## Retries and timing Temporary failures are retried after 1 minute, 5, 15 and 30 minutes, 1 and 2 hours, then every 4 hours, for up to **72 hours**; each one emits `email.delivery_delayed`. Permanent failures bounce immediately. If the primary route is blocked or unreachable, Pidgely can deliver through a second own node. --- # Templates > Store email content once with typed variables and fallbacks, edit drafts safely, publish versions and send with template id or alias. Source: https://docs.pidgely.com/guides/templates/ A template holds the subject, HTML, text and (optionally) sender of an email, with **variables** filled in at send time. Templates have a **draft** you can edit freely and **published versions** that sends use, so editing never changes what goes out until you publish. ## Create a template ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.templates.create({ name: 'Order confirmation', alias: 'order-confirmation', from: 'Acme ', subject: 'Order {{{ORDER_ID}}} confirmed', html: '

Hi {{{CUSTOMER_NAME|there}}}, your order {{{ORDER_ID}}} totals ${{{TOTAL}}}.

', variables: [ { key: 'ORDER_ID', type: 'string', }, { key: 'TOTAL', type: 'number', fallbackValue: 0, }, { key: 'CUSTOMER_NAME', type: 'string', fallbackValue: 'there', }, ], }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Templates.create({ "name": "Order confirmation", "alias": "order-confirmation", "from": "Acme ", "subject": "Order {{{ORDER_ID}}} confirmed", "html": "

Hi {{{CUSTOMER_NAME|there}}}, your order {{{ORDER_ID}}} totals ${{{TOTAL}}}.

", "variables": [ { "key": "ORDER_ID", "type": "string", }, { "key": "TOTAL", "type": "number", "fallback_value": 0, }, { "key": "CUSTOMER_NAME", "type": "string", "fallback_value": "there", }, ], }) print(result) ``` ```php tab="PHP" templates->create([ 'name' => 'Order confirmation', 'alias' => 'order-confirmation', 'from' => 'Acme ', 'subject' => 'Order {{{ORDER_ID}}} confirmed', 'html' => '

Hi {{{CUSTOMER_NAME|there}}}, your order {{{ORDER_ID}}} totals ${{{TOTAL}}}.

', 'variables' => [ [ 'key' => 'ORDER_ID', 'type' => 'string', ], [ 'key' => 'TOTAL', 'type' => 'number', 'fallback_value' => 0, ], [ 'key' => 'CUSTOMER_NAME', 'type' => 'string', 'fallback_value' => 'there', ], ], ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Templates.Create(ctx, &pidgely.CreateTemplateRequest{ Name: "Order confirmation", Alias: "order-confirmation", From: "Acme ", Subject: "Order {{{ORDER_ID}}} confirmed", Html: "

Hi {{{CUSTOMER_NAME|there}}}, your order {{{ORDER_ID}}} totals ${{{TOTAL}}}.

", Variables: []pidgely.TemplateVariableInput{ { Key: "ORDER_ID", Type: "string", }, { Key: "TOTAL", Type: "number", FallbackValue: 0, }, { Key: "CUSTOMER_NAME", Type: "string", FallbackValue: "there", }, }, }) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Templates.create({ name: "Order confirmation", alias: "order-confirmation", from: "Acme ", subject: "Order {{{ORDER_ID}}} confirmed", html: "

Hi {{{CUSTOMER_NAME|there}}}, your order {{{ORDER_ID}}} totals ${{{TOTAL}}}.

", variables: [ { key: "ORDER_ID", type: "string" }, { key: "TOTAL", type: "number", fallback_value: 0 }, { key: "CUSTOMER_NAME", type: "string", fallback_value: "there" } ] }) puts result ``` ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/templates' \ -H "Authorization: Bearer $PIDGELY_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "name": "Order confirmation", "alias": "order-confirmation", "from": "Acme ", "subject": "Order {{{ORDER_ID}}} confirmed", "html": "

Hi {{{CUSTOMER_NAME|there}}}, your order {{{ORDER_ID}}} totals ${{{TOTAL}}}.

", "variables": [ { "key": "ORDER_ID", "type": "string" }, { "key": "TOTAL", "type": "number", "fallback_value": 0 }, { "key": "CUSTOMER_NAME", "type": "string", "fallback_value": "there" } ] }' ``` - `name` (1–200 characters) and `html` are required. `from` and `reply_to` are validated as addresses when present. - `alias` is an optional, unique, human-readable key (`^[A-Za-z0-9][A-Za-z0-9_.-]{0,99}$`, not shaped like a UUID). Every endpoint accepts the id **or** the alias. A duplicate alias gets `409 alias_taken`. ## Variables Variables use **triple braces** in `subject`, `html` and `text`: | Syntax | Result | |---|---| | `{{{ORDER_ID}}}` | The value passed at send time (or the declared fallback) | | `{{{CUSTOMER_NAME\|there}}}` | The value, or `there` when none is available | | `{{ ORDER_ID }}` | Left as is (double braces are not variables) | Spaces inside the braces are allowed. In `html`, values are HTML-escaped; in `subject` and `text` they are inserted as is. Declare up to **50** variables with a `key` (`^[A-Za-z0-9_]{1,50}$`), a `type` (`string` or `number`) and an optional `fallback_value` of that type. The names `FIRST_NAME`, `LAST_NAME`, `EMAIL` and `UNSUBSCRIBE_URL` are reserved. ## Drafts and publishing 1. Every create or update changes the **draft** and sets `has_unpublished_versions: true`. 2. [`POST /templates/{id}/publish`](https://docs.pidgely.com/api-reference/templates/publish/) turns the draft into an immutable published version. 3. Sends always use the **latest published version**. A template that was never published can't be used (`template.id: only published templates can be used to send`). [Duplicate](https://docs.pidgely.com/api-reference/templates/duplicate/) creates a draft copy named ` (copy)`, handy for A/B variants. ## Send with a template ```json { "to": ["steve@example.com"], "template": { "id": "order-confirmation", "variables": { "ORDER_ID": "1234", "TOTAL": 59.9, "CUSTOMER_NAME": "Steve" } } } ``` - `template.id` takes the id or the alias. - `template` can't be combined with `html` or `text`: the template's bodies are always used. - `from`, `subject` and `reply_to` come from the template **only when the request leaves them empty**: values in the request win. - Each variable resolves to the value you pass, then the declared `fallback_value`, then the inline `|fallback`. If any is still missing, the send fails with `422 validation_error` and `template.variables: missing value for ORDER_ID`. - String values can be up to 2,000 characters; numbers are formatted as given. A string passed to a `number` variable fails with `must be a number`. Keys you didn't declare are treated as strings. Templates work in [batch sends](https://docs.pidgely.com/guides/sending/#batch-sending) too, each email with its own variables. --- # Contacts, segments & audiences > Store contacts with typed properties, group them in segments for broadcasts, and keep unsubscribes in sync with the suppression list. Legacy audiences are supported. Source: https://docs.pidgely.com/guides/contacts/ Contacts are the people you send [broadcasts](https://docs.pidgely.com/guides/broadcasts/) to: - **Contacts are global** to your account and unique by email. - **Segments** are named groups. A contact can belong to any number of segments. - **Audiences** are the legacy name for segments, kept so older code keeps working. ## Contacts ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.contacts.create({ email: 'steve@example.com', firstName: 'Steve', lastName: 'Wozniak', properties: { plan: 'pro', seats: 5, beta: true, }, segments: [ { id: '0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48', }, ], }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Contacts.create({ "email": "steve@example.com", "first_name": "Steve", "last_name": "Wozniak", "properties": { "plan": "pro", "seats": 5, "beta": True, }, "segments": [ { "id": "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48", }, ], }) print(result) ``` ```php tab="PHP" contacts->create([ 'email' => 'steve@example.com', 'first_name' => 'Steve', 'last_name' => 'Wozniak', 'properties' => [ 'plan' => 'pro', 'seats' => 5, 'beta' => true, ], 'segments' => [ [ 'id' => '0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48', ], ], ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Contacts.Create(ctx, &pidgely.CreateContactRequest{ Email: "steve@example.com", FirstName: "Steve", LastName: "Wozniak", Properties: map[string]any{ "plan": "pro", "seats": 5, "beta": true, }, Segments: []pidgely.Segment{ { Id: "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48", }, }, }) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Contacts.create({ email: "steve@example.com", first_name: "Steve", last_name: "Wozniak", properties: { plan: "pro", seats: 5, beta: true }, segments: [ { id: "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48" } ] }) puts result ``` ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/contacts' \ -H "Authorization: Bearer $PIDGELY_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "email": "steve@example.com", "first_name": "Steve", "last_name": "Wozniak", "properties": { "plan": "pro", "seats": 5, "beta": true }, "segments": [ { "id": "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48" } ] }' ``` - [`POST /contacts`](https://docs.pidgely.com/api-reference/contacts/create/) is an **upsert**: posting an email that already exists updates that contact. - `first_name` and `last_name` are up to 200 characters. - **Properties**: up to 100 per contact. Keys match `^[A-Za-z_][A-Za-z0-9_]{0,49}$`; values are strings (up to 2,000 characters), numbers or booleans. Set a key to `null` to delete it. Reads return each property as `{ "value": …, "type": "string" | "number" | "boolean" }`. - Retrieve, update and delete by **id or email**: `GET /contacts/steve@example.com` works. - `topics` is not supported yet and is rejected. ## Unsubscribes and the suppression list `unsubscribed` and the [suppression list](https://docs.pidgely.com/guides/suppressions/) stay in sync, both ways: | Change | Effect | |---|---| | The contact clicks the unsubscribe link of a broadcast | An `unsubscribe` suppression is added and the contact becomes `unsubscribed: true` | | You set `unsubscribed: true` | An `unsubscribe` suppression is added | | You set `unsubscribed: false` | The `unsubscribe` suppression is removed (bounce and complaint suppressions are never removed this way) | | You remove an `unsubscribe` suppression | The contact becomes subscribed again | | You create a contact whose address already has an `unsubscribe` suppression | The contact starts as unsubscribed | Suppressions are account-wide: an address that unsubscribed from broadcasts is also skipped by transactional sends. Deleting a contact keeps its suppression. ## Segments Create a segment, then add contacts to it: ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.segments.create({ name: 'Newsletter', }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Segments.create({ "name": "Newsletter", }) print(result) ``` ```php tab="PHP" segments->create([ 'name' => 'Newsletter', ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Segments.Create(ctx, &pidgely.CreateSegmentRequest{ Name: "Newsletter", }) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Segments.create({ name: "Newsletter" }) puts result ``` ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/segments' \ -H "Authorization: Bearer $PIDGELY_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "name": "Newsletter" }' ``` ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.contacts.segments.add({ email: 'steve@example.com', segmentId: '0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48', }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Contacts.Segments.add({ "email": "steve@example.com", "segment_id": "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48", }) print(result) ``` ```php tab="PHP" contacts->segments->add([ 'email' => 'steve@example.com', 'segment_id' => '0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48', ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Contacts.Segments.Add(ctx, "steve@example.com", "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48") if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Contacts::Segments.add({ email: "steve@example.com", segment_id: "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48" }) puts result ``` ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/contacts/steve@example.com/segments/0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` - List a segment's members with [`GET /segments/{id}/contacts`](https://docs.pidgely.com/api-reference/segments/list-contacts/) or `GET /contacts?segment_id=…`. - Removing a contact from a segment keeps the contact. Deleting a segment keeps its contacts. - Segment names are 1–200 characters. ## Audiences (legacy) The `/audiences` endpoints are aliases of segments over the same global contacts, and broadcasts accept `audience_id` as an alias of `segment_id`. > [!WARNING] > `DELETE /audiences/{id}/contacts/{email}` deletes the **whole contact**, not only its membership, because contacts are global. To remove someone from one segment only, use [`DELETE /contacts/{id}/segments/{segment_id}`](https://docs.pidgely.com/api-reference/contacts/remove-segment/). ## Webhooks Contact changes emit `contact.created`, `contact.updated` and `contact.deleted` [webhooks](https://docs.pidgely.com/guides/webhooks/#events) with the contact's id, email, names, `unsubscribed` and timestamps. --- # Broadcasts > Send campaigns to a segment with personalization variables, the {{{PIDGELY_UNSUBSCRIBE_URL}}} link and RFC 8058 one-click unsubscribe. Source: https://docs.pidgely.com/guides/broadcasts/ A broadcast sends the same content to every subscribed contact of a [segment](https://docs.pidgely.com/guides/contacts/#segments), personalized per contact. Broadcasts go out on the **broadcast stream** (separate IP pools from transactional mail) and every message carries one-click unsubscribe headers. ## Create and send ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.broadcasts.create({ segmentId: '0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48', name: 'October product update', from: 'Acme ', subject: 'What shipped in October, {{{FIRST_NAME|friend}}}', previewText: 'Five new features and a faster dashboard', html: '

Hi {{{FIRST_NAME|there}}},

Your plan: {{{contact.plan|free}}}.

Unsubscribe

', }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Broadcasts.create({ "segment_id": "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48", "name": "October product update", "from": "Acme ", "subject": "What shipped in October, {{{FIRST_NAME|friend}}}", "preview_text": "Five new features and a faster dashboard", "html": "

Hi {{{FIRST_NAME|there}}},

Your plan: {{{contact.plan|free}}}.

Unsubscribe

", }) print(result) ``` ```php tab="PHP" broadcasts->create([ 'segment_id' => '0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48', 'name' => 'October product update', 'from' => 'Acme ', 'subject' => 'What shipped in October, {{{FIRST_NAME|friend}}}', 'preview_text' => 'Five new features and a faster dashboard', 'html' => '

Hi {{{FIRST_NAME|there}}},

Your plan: {{{contact.plan|free}}}.

Unsubscribe

', ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Broadcasts.Create(ctx, &pidgely.CreateBroadcastRequest{ SegmentId: "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48", Name: "October product update", From: "Acme ", Subject: "What shipped in October, {{{FIRST_NAME|friend}}}", PreviewText: "Five new features and a faster dashboard", Html: "

Hi {{{FIRST_NAME|there}}},

Your plan: {{{contact.plan|free}}}.

Unsubscribe

", }) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Broadcasts.create({ segment_id: "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48", name: "October product update", from: "Acme ", subject: "What shipped in October, {{{FIRST_NAME|friend}}}", preview_text: "Five new features and a faster dashboard", html: "

Hi {{{FIRST_NAME|there}}},

Your plan: {{{contact.plan|free}}}.

Unsubscribe

" }) puts result ``` ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/broadcasts' \ -H "Authorization: Bearer $PIDGELY_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "segment_id": "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48", "name": "October product update", "from": "Acme ", "subject": "What shipped in October, {{{FIRST_NAME|friend}}}", "preview_text": "Five new features and a faster dashboard", "html": "

Hi {{{FIRST_NAME|there}}},

Your plan: {{{contact.plan|free}}}.

Unsubscribe

" }' ``` `segment_id` (or the legacy `audience_id`), `from` and `subject` are required. The broadcast is saved as a **draft**; then [send it](https://docs.pidgely.com/api-reference/broadcasts/send/) now or later: ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.broadcasts.send('0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19', { scheduledAt: 'tomorrow at 9am', }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Broadcasts.send("0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19", { "scheduled_at": "tomorrow at 9am", }) print(result) ``` ```php tab="PHP" broadcasts->send('0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19', [ 'scheduled_at' => 'tomorrow at 9am', ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Broadcasts.Send(ctx, "0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19", &pidgely.SendBroadcastRequest{ ScheduledAt: "tomorrow at 9am", }) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Broadcasts.send("0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19", { scheduled_at: "tomorrow at 9am" }) puts result ``` ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/broadcasts/0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19/send' \ -H "Authorization: Bearer $PIDGELY_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "scheduled_at": "tomorrow at 9am" }' ``` To create and send in one call, pass `"send": true` (and optionally `scheduled_at`) to `POST /broadcasts`. Sending requires a segment, a non-empty subject, `html` or `text`, and a `from` on a [verified domain](https://docs.pidgely.com/guides/domains/). `scheduled_at` takes ISO 8601 or [natural language](https://docs.pidgely.com/guides/sending/#scheduling), within 30 days. ## Personalization Variables use triple braces, with an optional fallback after `|`: | Variable | Value | |---|---| | `{{{FIRST_NAME}}}`, `{{{contact.first_name}}}` | Contact's first name | | `{{{LAST_NAME}}}`, `{{{contact.last_name}}}` | Contact's last name | | `{{{EMAIL}}}`, `{{{contact.email}}}` | Contact's email address | | `{{{contact.}}}` | Any [contact property](https://docs.pidgely.com/guides/contacts/#contacts), for example `{{{contact.plan}}}` | | `{{{PIDGELY_UNSUBSCRIBE_URL}}}` | This recipient's unsubscribe link (`{{{UNSUBSCRIBE_URL}}}` is an alias) | - `{{{FIRST_NAME|there}}}` uses `there` when the value is missing or empty. A variable with no value and no fallback renders as an empty string. - Values are HTML-escaped in `html` and `preview_text`, inserted as is in `subject` and `text`. Line breaks in the subject become spaces. - Variables work in `subject`, `html`, `text` and `preview_text`. `preview_text` is added as hidden text at the top of the HTML. ## Unsubscribe Every broadcast message carries `List-Unsubscribe` and `List-Unsubscribe-Post: List-Unsubscribe=One-Click` headers (covered by DKIM), so Gmail, Yahoo and Apple Mail show their native unsubscribe button. Clicking it unsubscribes the contact without a confirmation step, as [RFC 8058](https://pidgely.com/blog/one-click-unsubscribe-rfc-8058/) requires. > [!IMPORTANT] > Pidgely does **not** add an unsubscribe link to your HTML. Include `{{{PIDGELY_UNSUBSCRIBE_URL}}}` in a visible link: it is required by law in many countries and by the bulk-sender rules of Gmail and Yahoo. The link opens a confirmation page (a `GET` never unsubscribes anyone, so link scanners are harmless) with a button that confirms. The link in the body and the header point to the same delivery. Unsubscribing adds an `unsubscribe` [suppression](https://docs.pidgely.com/guides/suppressions/) and marks the contact `unsubscribed: true`. ## Who receives it - Contacts in the segment with `unsubscribed: false`, processed in pages of 500. - Addresses with a bounce, complaint or manual suppression are skipped at delivery (`email.suppressed`) and still appear in `recipients`. - Each message is a normal email with the tag `broadcast_id`, so delivery [webhooks](https://docs.pidgely.com/guides/webhooks/) work as for any other email. ## Lifecycle ```text draft ──send──► queued ──► sending ──► sent └──send with scheduled_at──► scheduled ──(time)──► queued scheduled / queued ──cancel──► canceled (errors) ──► failed ``` | Action | Allowed when | |---|---| | [Update](https://docs.pidgely.com/api-reference/broadcasts/update/) | `draft` (otherwise `409 broadcast_not_draft`) | | [Send](https://docs.pidgely.com/api-reference/broadcasts/send/) | `draft` | | [Cancel](https://docs.pidgely.com/api-reference/broadcasts/cancel/) | `scheduled` or `queued`; not once `sending` (`409 broadcast_not_cancelable`) | | [Delete](https://docs.pidgely.com/api-reference/broadcasts/delete/) | not `scheduled`, `queued` or `sending` (`409 broadcast_in_progress`) | | [Duplicate](https://docs.pidgely.com/api-reference/broadcasts/duplicate/) | always; creates a new draft without schedule | [Retrieve a broadcast](https://docs.pidgely.com/api-reference/broadcasts/retrieve/) to see its `status`, `recipients` and `stats` (deliveries by status). ## Quotas Broadcast messages count toward your [sending quotas](https://docs.pidgely.com/rate-limits/#sending-quotas). When a quota runs out mid-send, the broadcast **pauses** and resumes automatically at 00:00 UTC the next day. If the account is paused by the [reputation guard](https://docs.pidgely.com/rate-limits/#reputation-guard), the broadcast waits; if the account is suspended or validation fails, it ends as `failed`. --- # Webhooks > Receive signed events for deliveries, bounces, complaints, domains and contacts. Event list, payloads, svix-* / Standard Webhooks signature verification and retries. Source: https://docs.pidgely.com/guides/webhooks/ Webhooks push events to your server as they happen, so you don't have to poll. Webhooks created with [`POST /webhooks`](https://docs.pidgely.com/api-reference/webhooks/create/) are signed with the **[Standard Webhooks](https://www.standardwebhooks.com)** scheme (the same values are sent as `webhook-*` and `svix-*` headers), so any Standard Webhooks or Svix library can verify them. ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.webhooks.create({ endpoint: 'https://acme.com/webhooks/pidgely', events: ['email.delivered', 'email.bounced', 'email.complained'], }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Webhooks.create({ "endpoint": "https://acme.com/webhooks/pidgely", "events": ["email.delivered", "email.bounced", "email.complained"], }) print(result) ``` ```php tab="PHP" webhooks->create([ 'endpoint' => 'https://acme.com/webhooks/pidgely', 'events' => ['email.delivered', 'email.bounced', 'email.complained'], ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Webhooks.Create(ctx, &pidgely.CreateWebhookRequest{ Endpoint: "https://acme.com/webhooks/pidgely", Events: []string{"email.delivered", "email.bounced", "email.complained"}, }) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Webhooks.create({ endpoint: "https://acme.com/webhooks/pidgely", events: ["email.delivered", "email.bounced", "email.complained"] }) puts result ``` ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/webhooks' \ -H "Authorization: Bearer $PIDGELY_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "endpoint": "https://acme.com/webhooks/pidgely", "events": [ "email.delivered", "email.bounced", "email.complained" ] }' ``` The response contains the `signing_secret` (`whsec_…`). Store it with your other secrets: you need it to verify every request. ## Events | Event | Sent when | |---|---| | `email.sent` | The email was **accepted and queued** (one event per email, with the non-suppressed recipients) | | `email.delivered` | A recipient's server accepted the message | | `email.delivery_delayed` | A temporary failure; Pidgely will retry (for up to 72 hours) | | `email.bounced` | A permanent failure; the address is now [suppressed](https://docs.pidgely.com/guides/suppressions/) | | `email.complained` | The recipient marked the message as spam (feedback loop) | | `email.failed` | Pidgely gave up: retry window exhausted, account suspended or content expired | | `email.suppressed` | The recipient was skipped because the address is on the suppression list | | `domain.updated` | A domain's verification status changed | | `contact.created`, `contact.updated`, `contact.deleted` | A contact changed | `events` must list at least one event. `email.opened`, `email.clicked`, `email.received`, `email.scheduled`, `domain.created` and `domain.deleted` are accepted for compatibility but **never sent** (open/click tracking and inbound email are not available yet). ## Payloads Every request is a `POST` with a JSON body `{ type, created_at, data }`. Email events identify the email (`email_id`), the recipient and, where relevant, the delivery: ```json { "type": "email.delivered", "created_at": "2026-10-12T14:03:11.123456Z", "data": { "created_at": "2026-10-12T14:03:11.123456Z", "email_id": "0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21", "delivery_id": "0199d8a2-7b55-7e10-9c3d-1f2a3b4c5d6e", "recipient": "steve@example.com", "to": ["steve@example.com"], "transport": "mta", "remote_host": "gmail-smtp-in.l.google.com", "response": "250 2.0.0 OK" } } ``` Fields in `data` per event: | Event | `data` fields (besides `created_at`, `email_id`, `delivery_id`, `to`) | |---|---| | `email.sent` | `from`, `subject`, `to`, `tags`, `scheduled_at` | | `email.delivered` | `recipient`, `transport`, `remote_host`, `response` | | `email.delivery_delayed` | `recipient`, `attempt`, `class` (`transient`, `blocked`, `unreachable`), `code`, `enhanced_code`, `message`, `next_attempt_at` | | `email.bounced` | `recipient`, `type` (`hard`), `source` (`smtp`, `dsn`, `relay`), `code`, `enhanced_code`, `message` | | `email.complained` | `recipient`, `source` (`fbl`, `relay`) | | `email.failed` | `recipient`, `reason` (`retry_window_exhausted`, `account_suspended`, `content_expired`) | | `email.suppressed` | `recipient`, `reason` (`hard_bounce`, `complaint`, `unsubscribe`, `manual`) | | `domain.updated` | `domain_id`, `domain`, `status`, `previous` (`pending`, `verified`, `failed`) | | `contact.*` | `id`, `email`, `first_name`, `last_name`, `unsubscribed`, `created_at`, `updated_at` | ## Verify signatures Each request carries three headers, under both the Svix and the Standard Webhooks names: | Svix | Standard Webhooks | Content | |---|---|---| | `svix-id` | `webhook-id` | Message id (`evt_…`), the same on every retry | | `svix-timestamp` | `webhook-timestamp` | Unix seconds of this attempt | | `svix-signature` | `webhook-signature` | `v1,` (space-separated list) | The signature is an HMAC-SHA256, keyed with the base64-decoded part of the secret after `whsec_`, over `"{id}.{timestamp}.{raw body}"`. Verify against the **raw** request body (before JSON parsing), compare in constant time, and reject timestamps older than a few minutes to stop replays. The Node.js SDK does it for you (it is async, so remember the `await`): ```js import { verifyWebhook } from 'pidgely'; const event = await verifyWebhook({ payload: rawBody, headers, webhookSecret: process.env.PIDGELY_WEBHOOK_SECRET }); ``` The `svix` and `standardwebhooks` libraries work too. Or verify it yourself in any language: ```js tab="Node.js" import crypto from 'node:crypto'; import express from 'express'; function verify(rawBody, headers, secret) { const id = headers['svix-id'] ?? headers['webhook-id']; const ts = headers['svix-timestamp'] ?? headers['webhook-timestamp']; const sigs = headers['svix-signature'] ?? headers['webhook-signature']; if (!id || !ts || !sigs) throw new Error('missing signature headers'); if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) throw new Error('timestamp too old'); const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64'); const expected = crypto.createHmac('sha256', key).update(`${id}.${ts}.${rawBody}`).digest('base64'); const ok = sigs.split(' ').some((s) => { const sig = s.split(',')[1] ?? ''; return sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)); }); if (!ok) throw new Error('invalid signature'); return JSON.parse(rawBody); } const app = express(); app.post('/webhooks/pidgely', express.raw({ type: 'application/json' }), (req, res) => { let event; try { event = verify(req.body.toString('utf8'), req.headers, process.env.PIDGELY_WEBHOOK_SECRET); } catch { return res.sendStatus(400); } // deduplicate on req.headers['svix-id'], then handle event.type res.sendStatus(200); }); ``` ```python tab="Python" import base64, hashlib, hmac, json, time def verify(body: bytes, headers, secret: str): msg_id = headers.get("svix-id") or headers.get("webhook-id") ts = headers.get("svix-timestamp") or headers.get("webhook-timestamp") sigs = headers.get("svix-signature") or headers.get("webhook-signature") if not (msg_id and ts and sigs): raise ValueError("missing signature headers") if abs(time.time() - int(ts)) > 300: raise ValueError("timestamp too old") key = base64.b64decode(secret.removeprefix("whsec_")) mac = hmac.new(key, f"{msg_id}.{ts}.".encode() + body, hashlib.sha256).digest() expected = base64.b64encode(mac).decode() if not any(hmac.compare_digest(s.split(",", 1)[-1], expected) for s in sigs.split(" ")): raise ValueError("invalid signature") return json.loads(body) # Flask: event = verify(request.get_data(), request.headers, os.environ["PIDGELY_WEBHOOK_SECRET"]) ``` ```php tab="PHP" 300) throw new Exception('timestamp too old'); $key = base64_decode(preg_replace('/^whsec_/', '', $secret)); $expected = base64_encode(hash_hmac('sha256', "$id.$ts.$body", $key, true)); foreach (explode(' ', $sigs) as $s) { $parts = explode(',', $s, 2); if (isset($parts[1]) && hash_equals($expected, $parts[1])) return json_decode($body, true); } throw new Exception('invalid signature'); } $headers = array_change_key_case(getallheaders(), CASE_LOWER); $event = verify_webhook(file_get_contents('php://input'), $headers, getenv('PIDGELY_WEBHOOK_SECRET')); ``` ```go tab="Go" package webhooks import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "errors" "math" "net/http" "strconv" "strings" "time" ) func header(h http.Header, a, b string) string { if v := h.Get(a); v != "" { return v } return h.Get(b) } // Verify checks a Pidgely webhook. body must be the raw request body. func Verify(body []byte, h http.Header, secret string) error { id, ts, sigs := header(h, "svix-id", "webhook-id"), header(h, "svix-timestamp", "webhook-timestamp"), header(h, "svix-signature", "webhook-signature") if id == "" || ts == "" || sigs == "" { return errors.New("missing signature headers") } sec, err := strconv.ParseInt(ts, 10, 64) if err != nil || math.Abs(float64(time.Now().Unix()-sec)) > 300 { return errors.New("timestamp too old") } key, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(secret, "whsec_")) if err != nil { return err } mac := hmac.New(sha256.New, key) mac.Write([]byte(id + "." + ts + ".")) mac.Write(body) expected := base64.StdEncoding.EncodeToString(mac.Sum(nil)) for _, s := range strings.Split(sigs, " ") { if _, sig, ok := strings.Cut(s, ","); ok && hmac.Equal([]byte(sig), []byte(expected)) { return nil } } return errors.New("invalid signature") } ``` ```ruby tab="Ruby" require "base64" require "json" require "openssl" def verify_webhook(body, headers, secret) id = headers["svix-id"] || headers["webhook-id"] ts = headers["svix-timestamp"] || headers["webhook-timestamp"] sigs = headers["svix-signature"] || headers["webhook-signature"] raise "missing signature headers" unless id && ts && sigs raise "timestamp too old" if (Time.now.to_i - ts.to_i).abs > 300 key = Base64.strict_decode64(secret.delete_prefix("whsec_")) expected = Base64.strict_encode64(OpenSSL::HMAC.digest("SHA256", key, "#{id}.#{ts}.#{body}")) ok = sigs.split(" ").any? { |s| OpenSSL.secure_compare(s.split(",", 2).last.to_s, expected) } raise "invalid signature" unless ok JSON.parse(body) end # Rails: verify_webhook(request.raw_post, request.headers, ENV.fetch("PIDGELY_WEBHOOK_SECRET")) ``` More background in [Email webhooks: verifying signatures and handling retries](https://pidgely.com/blog/email-webhooks-signature-verification/). ## Delivery and retries - Requests come from Pidgely with `User-Agent: pidgely-webhooks/1` and `Content-Type: application/json`. - **Any `2xx`** response is a success. Answer quickly (the timeout is **10 seconds**) and do heavy work asynchronously. - Anything else (error status, timeout, connection failure) is retried up to **15 attempts** with exponential backoff and jitter: roughly 30–60 s, then 1–2 min, 2–4 min, … capped at one hour between attempts. Retries span about 4.5 to 9 hours in total. - **`410 Gone`** stops retries for that event immediately. - Delivery is **at least once** and order is not guaranteed: deduplicate on `svix-id` / `webhook-id`, which is the same on every attempt (so is the body). - Endpoints are never disabled automatically. A disabled or deleted endpoint drops its queued deliveries. - Rotating the secret ([`POST /webhooks/{id}/signing-secret/rotate`](https://docs.pidgely.com/api-reference/webhooks/rotate-secret/)) takes effect **immediately**, including for queued retries. ## Endpoint requirements The endpoint must be a public `https` URL without credentials. Private, loopback, link-local and other internal addresses are refused, both when you register the endpoint and on every delivery (which also defeats DNS rebinding). Redirects are not followed. ## Native events Webhooks created with the native [`POST /v1/webhooks`](https://docs.pidgely.com/api-reference/v1/webhooks/create/) use native names and include a top-level `id`: `email.queued` (= `email.sent`), `email.delivered`, `email.deferred` (= `email.delivery_delayed`), `email.bounced`, `email.failed`, `email.complained`, `email.suppressed`, `email.unsubscribed`, `email.canceled`, `domain.status_changed` (= `domain.updated`), `account.status_changed` and `contact.*`. An empty `events` list subscribes to all of them. The signature scheme is the same. --- # SMTP relay > Send through smtp.pidgely.com on ports 587 or 465 with user pidgely and your API key as password. Examples for Nodemailer, Django, Rails, Laravel and Spring. Source: https://docs.pidgely.com/guides/smtp/ If your framework or tool only speaks SMTP, send through Pidgely's submission server. Messages go through the same pipeline as the API: verified From domain, suppression list, quotas, DKIM signing and webhooks. | Setting | Value | |---|---| | Host | `smtp.pidgely.com` | | Port | `587` or `2587` with STARTTLS · `465` or `2465` with implicit TLS | | Username | `pidgely` | | Password | Your API key, `pdg_…` | | Authentication | `PLAIN` or `LOGIN`, TLS 1.2 or newer | Any valid API key works, including `sending_access` keys; a [domain-restricted key](https://docs.pidgely.com/authentication/#domain-restricted-keys) can only send from its domain. Use ports `2587`/`2465` if your network blocks the standard ones. ## Framework examples ```js tab="Nodemailer" import nodemailer from 'nodemailer'; const transporter = nodemailer.createTransport({ host: 'smtp.pidgely.com', port: 465, secure: true, auth: { user: 'pidgely', pass: process.env.PIDGELY_API_KEY }, }); await transporter.sendMail({ from: 'Acme ', to: 'steve@example.com', subject: 'Hello over SMTP', html: '

It works!

', headers: { 'Pidgely-Idempotency-Key': 'welcome-42', 'X-Pidgely-Tags': 'category=welcome' }, }); ``` ```python tab="Django" # settings.py import os EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" EMAIL_HOST = "smtp.pidgely.com" EMAIL_PORT = 587 EMAIL_USE_TLS = True EMAIL_HOST_USER = "pidgely" EMAIL_HOST_PASSWORD = os.environ["PIDGELY_API_KEY"] DEFAULT_FROM_EMAIL = "Acme " ``` ```ruby tab="Rails" # config/environments/production.rb config.action_mailer.delivery_method = :smtp config.action_mailer.smtp_settings = { address: "smtp.pidgely.com", port: 587, user_name: "pidgely", password: ENV.fetch("PIDGELY_API_KEY"), authentication: :plain, enable_starttls_auto: true } ``` ```env tab="Laravel" # .env (STARTTLS is negotiated automatically on port 587) MAIL_MAILER=smtp MAIL_HOST=smtp.pidgely.com MAIL_PORT=587 MAIL_USERNAME=pidgely MAIL_PASSWORD=pdg_your_key_here MAIL_FROM_ADDRESS=hello@acme.com MAIL_FROM_NAME="Acme" ``` ```properties tab="Spring" # application.properties spring.mail.host=smtp.pidgely.com spring.mail.port=587 spring.mail.username=pidgely spring.mail.password=${PIDGELY_API_KEY} spring.mail.properties.mail.smtp.auth=true spring.mail.properties.mail.smtp.starttls.enable=true spring.mail.properties.mail.smtp.starttls.required=true ``` ## Special headers | Header | Effect | |---|---| | `Pidgely-Idempotency-Key` | Deduplicates retries for 24 hours; a repeat returns the original message id. See [Idempotency](https://docs.pidgely.com/idempotency/). | | `X-Pidgely-Tags: name=value,name2=value2` | Sets [tags](https://docs.pidgely.com/guides/sending/#tags) (`[A-Za-z0-9_-]{1,256}` each). | Pidgely removes `Bcc`, `Return-Path`, `DKIM-Signature`, `Received`, `Delivered-To`, `Authentication-Results`, `ARC-*`, `Feedback-ID`, `List-Unsubscribe-Post` and the two headers above before sending, and adds `Message-ID` and `Date` when missing. Bcc recipients are taken from the SMTP envelope. Other headers (for example `X-Entity-Ref-ID`) pass through. SMTP mail uses the transactional stream. ## Limits - Messages up to **40 MB**. - Up to **50 recipients** per message (after de-duplication). - Sending [quotas](https://docs.pidgely.com/rate-limits/#sending-quotas) apply as for the API. ## Error replies | Reply | Meaning | |---|---| | `530 5.7.0` | Authenticate first | | `535 5.7.8` | Wrong username or API key | | `454 4.7.0` | Temporary authentication failure; retry | | `550 5.6.0` | Invalid message, including a From domain that is not verified | | `550 5.7.1` | Not allowed: key restricted to another domain, account paused or suspended | | `452 4.5.3` | Sending quota reached | | `451 4.7.0` | Rate limited; retry later | | `451 4.3.0` | Temporary failure; retry | --- # Suppressions > Hard bounces, spam complaints and unsubscribes are suppressed automatically, account-wide. How suppression affects sends and how to manage the list. Source: https://docs.pidgely.com/guides/suppressions/ The suppression list holds addresses Pidgely won't deliver to. It protects your sender reputation: sending again to an address that bounced or complained is the fastest way to land in spam. ## What gets suppressed | Origin | Added when | |---|---| | `bounce` | A **hard bounce**: the receiving server rejected the address permanently (SMTP `5xx` that isn't a policy block or a full mailbox), or an asynchronous bounce report (DSN) arrived | | `complaint` | The recipient marked a message as spam (feedback loop) | | `unsubscribe` | The recipient used a one-click unsubscribe link, or you set a contact to `unsubscribed: true` | | `manual` | You added the address ([`POST /suppressions`](https://docs.pidgely.com/api-reference/suppressions/create/)) | Temporary failures (soft bounces, full mailboxes) and blocks caused by reputation or policy are **retried, not suppressed**. There is no soft-bounce counter. See [Hard vs soft bounces](https://pidgely.com/blog/hard-vs-soft-bounces/). ## How it affects sending - The list is **per account and covers every stream**: an address that unsubscribed from a broadcast is also skipped for transactional mail. - Sends to a suppressed address are **accepted**, not rejected: that recipient ends as `suppressed`, emits `email.suppressed` and does not count toward your quota. The rest of the email is delivered normally. - The list is checked when you send and again before each delivery attempt. - An address is suppressed once: the first origin recorded wins, and adding it again is a no-op. ## Managing the list | Task | Endpoint | |---|---| | List, optionally `?origin=bounce` | [`GET /suppressions`](https://docs.pidgely.com/api-reference/suppressions/list/) | | Look up by id or email | [`GET /suppressions/{id_or_email}`](https://docs.pidgely.com/api-reference/suppressions/retrieve/) | | Add one / up to 100 | [`POST /suppressions`](https://docs.pidgely.com/api-reference/suppressions/create/), [`POST /suppressions/batch/add`](https://docs.pidgely.com/api-reference/suppressions/batch-add/) | | Remove one / up to 100 | [`DELETE /suppressions/{id_or_email}`](https://docs.pidgely.com/api-reference/suppressions/delete/), [`POST /suppressions/batch/remove`](https://docs.pidgely.com/api-reference/suppressions/batch-remove/) | Batch add is not atomic: it stops at the first invalid address and keeps those added before it. Batch remove skips entries that don't exist. > [!WARNING] > Only remove a `bounce` or `complaint` suppression when you are sure the address is valid and wants your mail (for example, the person contacted you). Re-sending to bad addresses raises your bounce and complaint rates, and the [reputation guard](https://docs.pidgely.com/rate-limits/#reputation-guard) pauses sending above 4% bounces or 0.1% complaints. Removing an `unsubscribe` suppression re-subscribes the matching [contact](https://docs.pidgely.com/guides/contacts/#unsubscribes-and-the-suppression-list). --- # Deliverability best practices > What Pidgely does for your inbox placement, and the practices on your side that matter most - authentication, list hygiene, separate streams, complaints and warm-up. Source: https://docs.pidgely.com/guides/deliverability/ Pidgely handles the infrastructure side of deliverability. The rest depends on what you send and to whom. This page lists both, with links to longer articles on the [Pidgely blog](https://pidgely.com/blog/). ## What Pidgely does for you - **Authentication**: DKIM (RSA-2048) per domain and an SPF-aligned return path, so DMARC passes on both. See [Domains & DNS](https://docs.pidgely.com/guides/domains/). - **Separate streams**: transactional and broadcast mail use separate IP pools. See [Transactional vs marketing streams](https://pidgely.com/blog/transactional-vs-marketing-streams/). - **IP warm-up and pacing**: new IPs ramp up gradually, concurrency is capped per destination, and each account sends at its own pace. See [How to warm up a new sending IP](https://pidgely.com/blog/ip-warmup-guide/). - **Bounce and complaint processing**: VERP return paths, DSN parsing and feedback loops feed the [suppression list](https://docs.pidgely.com/guides/suppressions/) automatically. See [VERP and automated bounce handling](https://pidgely.com/blog/verp-bounce-handling/) and [Feedback loops](https://pidgely.com/blog/feedback-loops-complaint-rate/). - **One-click unsubscribe** (RFC 8058) on every broadcast. See [One-click unsubscribe](https://pidgely.com/blog/one-click-unsubscribe-rfc-8058/). - **Reputation guard**: sending pauses automatically above 4% bounces or 0.1% complaints, before the damage spreads ([details](https://docs.pidgely.com/rate-limits/#reputation-guard)). ## Checklist for your side 1. **Publish DMARC** on your domain. Start with `p=none` and a reporting address, then tighten. Gmail and Yahoo require it for bulk senders: [the checklist](https://pidgely.com/blog/gmail-yahoo-bulk-sender-requirements/). 2. **Send only to people who asked.** Use double opt-in for newsletters; never buy lists. 3. **Keep complaints under 0.1%.** Make unsubscribing easy: include `{{{PIDGELY_UNSUBSCRIBE_URL}}}` visibly in every broadcast. 4. **Separate transactional and marketing mail.** Use broadcasts (or `"stream": "broadcast"`) for marketing; consider a subdomain such as `news.acme.com` for it. 5. **Use a consistent From address** with a real name and a monitored `reply_to`. 6. **Send both HTML and text** (Pidgely generates text from HTML if you don't), keep HTML light, and avoid link shorteners. 7. **Remove inactive recipients** from marketing segments after a few months of silence. 8. **Watch webhooks**: track `email.bounced`, `email.complained` and `email.delivery_delayed` per campaign. 9. **Ramp up new volume** gradually instead of sending your whole list on day one. Going to spam already? Work through [Why your emails go to spam](https://pidgely.com/blog/why-emails-go-to-spam/). Wondering about dedicated IPs? See [Shared vs dedicated IP](https://pidgely.com/blog/shared-vs-dedicated-ip/). --- # Migrate from Resend > Move from Resend to Pidgely - switch to the Pidgely SDK (same method names), move domains, webhooks, contacts and suppressions, and the differences to know. Source: https://docs.pidgely.com/guides/migrate-from-resend/ Pidgely serves the Resend REST API at the same paths, with no version prefix. Requests, responses, status codes, the error body (`{"statusCode","name","message"}`), the `Idempotency-Key` header and cursor pagination all match. To migrate, switch to the Pidgely SDK for your language (same method names) and swap the API key; your code barely changes. ## Step by step 1. **Create an account and an API key.** Pidgely keys look like `pdg_xxxxxxxx_xxxx…`; the SDKs don't check the `re_` prefix. 2. **Add your domain** ([`POST /domains`](https://docs.pidgely.com/api-reference/domains/create/) or the dashboard) and publish the records it returns: DKIM `TXT`, the return-path `MX` + SPF `TXT`, and optionally DMARC. The return path defaults to `bounces.`; pass `"custom_return_path": "send"` to reuse the label you had on Resend. You can keep the Resend records in place during the switch: the DKIM selectors differ, so both coexist. 3. **Verify** the domain ([`POST /domains/{id}/verify`](https://docs.pidgely.com/api-reference/domains/verify/)). 4. **Switch your code** to the [Pidgely SDK](#switch-to-the-pidgely-sdk-recommended) (or, temporarily, point the Resend SDK at Pidgely) and deploy. 5. **Recreate webhooks** with [`POST /webhooks`](https://docs.pidgely.com/api-reference/webhooks/create/) and update the signing secret in your receiver. Verification code doesn't change. 6. **Import contacts and segments** if you use broadcasts ([`POST /contacts`](https://docs.pidgely.com/api-reference/contacts/create/) is an upsert, so re-running an import is safe), and copy your suppression list ([`POST /suppressions/batch/add`](https://docs.pidgely.com/api-reference/suppressions/batch-add/)) so bounced and unsubscribed addresses stay suppressed. 7. **Recreate templates** and publish them; aliases keep your send code unchanged. ## Switch to the Pidgely SDK (recommended) The official [Pidgely SDKs](https://docs.pidgely.com/sdks/) mirror the Resend SDKs' method names, arguments and `{ data, error }` results, so switching is a change of package, import and key: ```diff - import { Resend } from 'resend'; - const resend = new Resend(process.env.RESEND_API_KEY); + import { Pidgely } from 'pidgely'; + const resend = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await resend.emails.send({ from, to, subject, html }); ``` 1. `npm uninstall resend && npm install pidgely` (or the [package for your language](https://docs.pidgely.com/sdks/)). 2. Rename the import and swap the `re_…` key for your `pdg_…` key. 3. If you verify webhooks with the SDK, `await` the `verify` call (it is async in the Pidgely SDK). You also get Pidgely additions such as automatic retries, `emails.attachments`, `apiKeys.update`, `broadcasts.cancel/duplicate` and `suppressions.*`. ## Keep the Resend SDK for now (base URL per SDK) If you can't change dependencies yet, the official Resend SDKs also work against Pidgely: set their base URL to `https://api.pidgely.com` and use a Pidgely key. Plan to switch to the Pidgely SDK afterwards. | SDK | Override | Example | |---|---|---| | Node.js (`resend`) | env `RESEND_BASE_URL`, or `new Resend(key, { baseUrl })` | `RESEND_BASE_URL=https://api.pidgely.com RESEND_API_KEY=pdg_… node app.js` | | Python (`resend`) | env `RESEND_API_URL`, or `resend.api_url = "…"` | `resend.api_url = "https://api.pidgely.com"` | | Go (`resend-go`) | env `RESEND_BASE_URL` (read at init), or `client.BaseURL, _ = url.Parse("https://api.pidgely.com/")` | the trailing slash matters when you set `BaseURL` directly | | PHP / Laravel (`resend-php`, `resend-laravel`) | env `RESEND_BASE_URL` (host or full URL) | `RESEND_BASE_URL=https://api.pidgely.com/` | | Ruby / Rails (`resend`) | env `RESEND_BASE_URL` (read at load time) | `RESEND_BASE_URL=https://api.pidgely.com/` | | Rust (`resend-rs`) | env `RESEND_BASE_URL`, or `ConfigBuilder::new(key).base_url(url)` | `.base_url("https://api.pidgely.com".parse()?)` | | Java (`resend-java`) | `Resend.builder().apiKey(key).baseUrl("https://api.pidgely.com").build()` | | | .NET (`Resend`) | `ResendClientOptions.ApiUrl` | `services.Configure(o => { o.ApiUrl = "https://api.pidgely.com"; o.ApiToken = key; });` | | Elixir (community `resend` package) | not verified: check your package version for a base URL option, or call the API with `Req`/`Finch` | | | cURL / raw HTTP | replace `https://api.resend.com` | `curl https://api.pidgely.com/emails -H "Authorization: Bearer pdg_…"` | ## SMTP | Resend | Pidgely | |---|---| | host `smtp.resend.com` | `smtp.pidgely.com` | | ports 465/2465 (TLS), 587/2587 (STARTTLS) | same | | user `resend`, password = API key | user `pidgely` (`resend` also works), password = API key | | `Resend-Idempotency-Key` header | same, or `Pidgely-Idempotency-Key` | See [SMTP relay](https://docs.pidgely.com/guides/smtp/). ## Webhooks Webhooks created through `POST /webhooks` use Resend's event names (`email.sent`, `email.delivered`, `email.delivery_delayed`, `email.bounced`, `email.complained`, `email.failed`, `email.suppressed`, `domain.updated`, `contact.created/updated/deleted`) and Resend's payload (`{type, created_at, data: {email_id, to[], …}}`). They are signed with the same Svix / Standard Webhooks scheme: `svix-id`, `svix-timestamp` and `svix-signature` headers, plus the `webhook-*` equivalents. `resend.webhooks.verify(…)` and the Svix libraries therefore work with your `whsec_…` secret. See [Webhooks](https://docs.pidgely.com/guides/webhooks/). ## Supported resources | Resource | Endpoints | Notes | |---|---|---| | Emails | send, batch (`x-batch-validation: strict\|permissive`), retrieve, list, update (`scheduled_at`), cancel, attachments (list/retrieve) | `scheduled_at` accepts ISO 8601 and natural language (`in 1 hour`, `tomorrow at 9am`). Attachments: base64 `content` or remote `path` (fetched with SSRF protection, 40 MB budget). `react` is rendered by the Node SDK as usual. | | Domains | create, list, retrieve, update (`tls`), delete, verify | `tls: enforced` is honoured by the MTA. `region` is informative. | | API keys | create (`full_access`/`sending_access`, `domain_id`), list, update (name), delete | | | Webhooks | create, list, retrieve, update, delete, rotate signing secret | | | Suppressions | add, batch add/remove, list (`origin`), retrieve/remove by id or email | Adds the `unsubscribe` origin. | | Contacts | create, list (`segment_id`), retrieve/update/delete by id or email, segment membership | `unsubscribed` stays in sync with the suppression list both ways. | | Segments | create, list, retrieve, update, delete, list contacts | | | Audiences (legacy) | create, list, retrieve, delete, audience-scoped contacts | Aliases of segments over global contacts. | | Broadcasts | create (`send`, `scheduled_at`), list, retrieve, update (drafts), delete, send, cancel, duplicate | `{{{FIRST_NAME\|there}}}`, `{{{contact.}}}`, `{{{RESEND_UNSUBSCRIBE_URL}}}` and the alias `{{{PIDGELY_UNSUBSCRIBE_URL}}}`; RFC 8058 one-click unsubscribe on every message. | | Templates | create, list, retrieve, update, delete, publish, duplicate; `template: {id, variables}` in send | Draft plus published versions; typed variables with fallbacks. | ## Not supported yet Each of these fails loudly rather than silently, so a migration can't misbehave unnoticed: - **Open and click tracking.** `open_tracking`/`click_tracking` are accepted and reported as `false`. `email.opened`/`email.clicked` webhooks can be created but never fire. - **Topics.** A non-empty `topic_id` or `topics` is rejected with 422. - **Inbound and receiving.** Inboxes, received emails, `capabilities.receiving: enabled` (rejected) and `email.received`. - **Other endpoints.** Automations, events, contact properties as a separate resource, contact imports, logs, OAuth, `POST /emails/{id}/share`, `GET /emails/metrics`, broadcast recipients and clicked links, domain claims, and webhook event history and replay. ## Differences worth knowing - Delete endpoints return `200` with `{"object","id","deleted":true}`, as Resend does. The native `/v1` API returns `204`. - Validation errors are `422 validation_error`. A From domain that is not verified, a plan limit, or a paused account returns `403 validation_error`. A `sending_access` key on a full-access endpoint returns `401 restricted_api_key`. - `GET /emails/{id}` reads from the primary database, so it is consistent immediately after a send. List endpoints may use a read replica. - `Idempotent-Replayed: true` marks a replayed idempotent response. - `email.sent` means **accepted and queued**; `email.delivered` confirms the receiving server accepted it. - Suppressions apply to every stream: an unsubscribe also stops transactional mail to that address. --- # SDKs > Official Pidgely SDKs for Go (published), Node.js, Python, PHP and Ruby (preview) - install, configure and send your first email. Source: https://docs.pidgely.com/sdks/ | Language | Install | Repository | Status | |---|---|---|---| | Go | `go get github.com/pidgely/pidgely-go` | [pidgely/pidgely-go](https://github.com/pidgely/pidgely-go) | **v0.1 — published** | | Node.js / TypeScript | `npm install pidgely` | [pidgely/pidgely-node](https://github.com/pidgely/pidgely-node) | Preview (0.1) — publishing soon | | Python | `pip install pidgely` | [pidgely/pidgely-python](https://github.com/pidgely/pidgely-python) | Preview (0.1) — publishing soon | | PHP | `composer require pidgely/pidgely-php guzzlehttp/guzzle` | [pidgely/pidgely-php](https://github.com/pidgely/pidgely-php) | Preview (0.1) — publishing soon | | Ruby | `gem install pidgely` | [pidgely/pidgely-ruby](https://github.com/pidgely/pidgely-ruby) | Preview (0.1) — publishing soon | > [!PREVIEW] > The Go SDK is published (v0.1.0). The Node.js, Python, PHP and Ruby SDKs are open source and tested, and are being published to their registries now; until then, install them from their repositories. The [cURL](#plain-http) examples always work. All SDKs share the same resources and method names (`emails.send`, `emails.get`, `batch.send`, `domains.create`, `domains.verify`, `apiKeys.create`, `contacts.*`, `segments.*`, `broadcasts.*`, `templates.*`, `webhooks.*`, `suppressions.*`), adapted to each language's conventions. Every page of the [API reference](https://docs.pidgely.com/api-reference/) shows the call in each language. ## Node.js ```bash npm install pidgely ``` ```js import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); // or set PIDGELY_API_KEY and call new Pidgely() const { data, error } = await pidgely.emails.send({ from: 'Acme ', to: ['delivered@example.com'], subject: 'Hello from Pidgely', html: '

It works!

', }); if (error) console.error(error); // { statusCode, name, message } else console.log(data.id); ``` - **Zero dependencies**, uses the global `fetch`: Node.js 18+, Bun, Deno, Cloudflare Workers, Vercel Edge. - **Never throws** on API or network errors: methods resolve to `{ data, error, headers }`. - **Idempotency and retries**: send methods accept `{ idempotencyKey }`; requests are retried with backoff (honouring `Retry-After`) up to `maxRetries` (default 2). A `POST` without an idempotency key is never retried after a 5xx or network error. - **Inputs in camelCase** (`replyTo`, `scheduledAt`, `segmentId`), responses exactly as the API returns them. - **React Email**: pass `react` instead of `html` (renders with `@react-email/render`). - **Webhook verification**: `await verifyWebhook({ payload, headers, webhookSecret })` (see [Webhooks](https://docs.pidgely.com/guides/webhooks/#verify-signatures)). ```js const pidgely = new Pidgely(process.env.PIDGELY_API_KEY, { baseUrl: 'https://api.pidgely.com', // or PIDGELY_BASE_URL timeout: 30_000, // per attempt, ms maxRetries: 2, }); ``` ## Python ```bash pip install pidgely ``` ```python import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] email = pidgely.Emails.send({ "from": "Acme ", "to": ["delivered@example.com"], "subject": "Hello from Pidgely", "html": "

It works!

", }) ``` ## PHP ```bash # Guzzle is the suggested HTTP client; any PSR-18 client works composer require pidgely/pidgely-php guzzlehttp/guzzle ``` ```php emails->send([ 'from' => 'Acme ', 'to' => ['delivered@example.com'], 'subject' => 'Hello from Pidgely', 'html' => '

It works!

', ]); ``` ## Go ```bash go get github.com/pidgely/pidgely-go ``` ```go package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) sent, err := client.Emails.Send(ctx, &pidgely.SendEmailRequest{ From: "Acme ", To: []string{"delivered@example.com"}, Subject: "Hello from Pidgely", Html: "

It works!

", }, pidgely.WithIdempotencyKey("hello-1")) if err != nil { panic(err) // *pidgely.Error carries StatusCode and Name } fmt.Println(sent.Id) } ``` Request options such as `pidgely.WithIdempotencyKey(…)` and `pidgely.WithBatchValidation(pidgely.BatchPermissive)` go after the request; list methods take `*pidgely.ListParams` (or `nil`). ## Ruby ```bash gem install pidgely ``` ```ruby require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] email = Pidgely::Emails.send({ from: "Acme ", to: ["delivered@example.com"], subject: "Hello from Pidgely", html: "

It works!

" }) ``` ## Plain HTTP Every API reference page also has the raw request as cURL. The [OpenAPI document](https://docs.pidgely.com/openapi.yaml) ([JSON](https://docs.pidgely.com/openapi.json)) describes the whole API and can generate a client for any other language. ## Coming from another provider? Moving existing code from Resend? See [Migrate from Resend](https://docs.pidgely.com/guides/migrate-from-resend/). --- # MCP server > Let AI assistants and agents send email and manage Pidgely through the Model Context Protocol. Configuration for Claude Desktop, Claude Code and Cursor (preview). Source: https://docs.pidgely.com/mcp/ > [!PREVIEW] > The Pidgely MCP server (`@pidgely/mcp`) is being built in the open at [pidgely/mcp](https://github.com/pidgely/mcp) and is not published yet. The configuration below is the **planned** setup; tool names and options may change before the first release. The [Model Context Protocol](https://modelcontextprotocol.io) lets AI assistants call external tools. With the Pidgely MCP server, an assistant can send emails and work with your Pidgely resources (emails, domains, contacts, broadcasts) using your API key. It runs locally over stdio with `npx` and reads the key from `PIDGELY_API_KEY`. ## Claude Desktop Add the server to `claude_desktop_config.json` (Settings → Developer → Edit config) and restart Claude Desktop: ```json title="claude_desktop_config.json" { "mcpServers": { "pidgely": { "command": "npx", "args": ["-y", "@pidgely/mcp"], "env": { "PIDGELY_API_KEY": "pdg_your_key_here" } } } } ``` ## Claude Code ```bash claude mcp add --transport stdio pidgely --env PIDGELY_API_KEY=pdg_your_key_here -- npx -y @pidgely/mcp ``` ## Cursor Add it to `.cursor/mcp.json` in your project (or `~/.cursor/mcp.json` for every project): ```json title=".cursor/mcp.json" { "mcpServers": { "pidgely": { "command": "npx", "args": ["-y", "@pidgely/mcp"], "env": { "PIDGELY_API_KEY": "pdg_your_key_here" } } } } ``` ## Choosing a key for an agent An agent acts with the permissions of its key. Start narrow: - For agents that only send, use a [`sending_access` key restricted to one domain](https://docs.pidgely.com/authentication/#domain-restricted-keys): it can't read contacts, change domains or create keys. - Use a `full_access` key only for agents you trust to manage the account, and revoke it when you're done. - Ask the assistant to confirm before sending to real recipients. ## Docs for assistants Assistants can also read these docs directly: [`/llms.txt`](https://docs.pidgely.com/llms.txt) lists every page, [`/llms-full.txt`](https://docs.pidgely.com/llms-full.txt) has all of them in one file, and every page is available as Markdown at `.md`. --- # Architecture > How Pidgely accepts, paces, signs, routes and tracks email - one Go binary in stateless roles over PostgreSQL, with its own MTA and feedback loop. Source: https://docs.pidgely.com/platform/architecture/ Pidgely is one Go binary and a PostgreSQL database. Knowing how it works helps you reason about timing, retries and what each webhook means. ```text HTTPS (REST API, SDKs) SMTP :25 (recipient MX) your app ───────────────► api ──┐ ┌────────► MTA (IP pools, warm-up) your app ── SMTP 587/465 ► smtp ─┤ PostgreSQL │ │ blocked / unreachable ├──► messages, deliveries, events │ ▼ │ contacts, broadcasts, jobs ◄───┴─ worker ─► second own node (fallback) mailbox providers ─ DSN/ARF ─► inbound └──► signed webhooks ──► your app ``` ## Roles The binary runs in roles: `api` (HTTP API), `smtp` (submission), `worker` (delivery, webhooks, broadcast fan-out), `inbound` (bounces and complaints) or `all` on one node. Every role is stateless and keeps its state in PostgreSQL, so each one scales horizontally on its own ([Scaling](https://docs.pidgely.com/platform/scaling/)). ## Life of an email 1. **Accept.** The API and SMTP share one pipeline. It validates the message (verified From domain, allowed for the key; recipient count; headers; attachments; template), then in **one transaction** reserves the quota, drops suppressed recipients, writes the message with one delivery per recipient, and enqueues one delivery job per recipient. You get the id only after that commit, so an accepted email is never lost. 2. **Pace.** Each account books delivery slots at its own rate, so a burst from one customer never delays the others. 3. **Deliver.** Before each attempt the worker re-checks cancellation, the schedule, account status, suppression and domain verification. It adds per-recipient headers (`Feedback-ID`, and `List-Unsubscribe` on the broadcast stream), signs DKIM and sends with a VERP return path. 4. **Route.** Pidgely's own MTA is tried first, with per-stream IP pools, warm-up caps and per-destination concurrency. A *blocked* or *unreachable* result can fall back to a second own node; a *permanent* result becomes a bounce and a suppression; a *transient* result is retried with backoff for up to 72 hours. 5. **Feedback.** Asynchronous bounces (DSN) arrive on the VERP address, complaints arrive through feedback loops (ARF), unsubscribes through the one-click link. Each updates the delivery, the suppression list and usage, and emits events and [webhooks](https://docs.pidgely.com/guides/webhooks/). Jobs are delivered **at least once** and every handler is idempotent, which is why webhooks carry a stable id for de-duplication. ## Deliverability by design - DKIM RSA-2048 per domain (relaxed/relaxed, From/Subject/To oversigned, `List-Unsubscribe` signed). - Return path on a subdomain of your domain (MX to Pidgely, SPF include), so SPF and DKIM both align for DMARC. - A text part generated from HTML; Message-ID on your domain; CRLF line endings. - EHLO with each IP's PTR name; opportunistic TLS by default, `tls: enforced` per domain. - Separate IP pools for transactional and broadcast mail; gradual warm-up of new IPs. ## Security - API keys and session tokens are stored only as HMAC/SHA-256 digests. DKIM private keys and webhook secrets are encrypted at rest (AES-256-GCM). - Unsubscribe, bounce (VERP) and attachment-download tokens are HMAC-signed. - Outbound requests (webhooks, remote attachments) pass an SSRF guard: the IP is checked after DNS resolution, private ranges are blocked and redirects are not followed. - Reserved headers and CR/LF injection are rejected, executable attachments are blocked, SMTP submissions are sanitized. - New accounts start in a sandbox with plan quotas, and the reputation guard pauses accounts with abnormal bounce or complaint rates. See [Security](https://docs.pidgely.com/security/) for how to report a vulnerability. ## Self-hosting Pidgely runs as a single binary with PostgreSQL 18, from a single small server up to separate fleets per role. Self-hosted deployments are not generally available during early access; [contact us](mailto:contact@pidgely.com) if you need one. ## Multi-channel (roadmap) The core is channel-agnostic: messages have a channel, there is one delivery per recipient, and events, suppression, quotas and webhooks are generic. SMS, WhatsApp and push are planned on the same model. They are not available yet. --- # Scaling > How Pidgely grows from one node to independent fleets by configuration, not rewrites - roles, per-kind workers, read replicas, partitions and an external broker. Source: https://docs.pidgely.com/platform/scaling/ Pidgely is designed to grow by **configuration, not rewrites**. Every stage below runs the same code; the next stage is adopted when a metric calls for it. This is a summary for the curious: as a customer of the hosted service, you don't need to do anything. | Stage | Layout | Move on when | |---|---|---| | 1. Single node | `pidgely all` + PostgreSQL on one server | CPU above 60%, delivery queue lag above a minute at peak, or zero-downtime API deploys needed | | 2. Split roles | `api` replicas behind a load balancer; `worker`, `inbound` and `smtp` on servers with clean IPs and port 25 | One kind of work starves another (a large broadcast delays transactional mail) | | 3. Per-kind worker fleets | Separate fleets for delivery, webhooks and broadcast fan-out, each scaled on its own queue lag | The primary database is busy with reads | | 4. Read replica | List endpoints read from a replica; single-object reads stay on the primary | Monthly partitions grow very large | | 5. Retention and partitions | Monthly partitions dropped by retention instead of row deletes | The job table becomes the bottleneck (thousands of jobs per second) | | 6. External broker | Jobs still written transactionally (outbox), relayed to a message broker | A context needs independent scaling | | 7. Extracted services | Webhooks, marketing or inbound run as their own services | — | ## What makes it possible - **Stateless roles** with health, readiness and metrics endpoints. - **Transactional outbox**: jobs are written in the same transaction as the change that causes them, so nothing is lost or duplicated by a crash. - **Idempotent job handlers**: every job tolerates running twice, late, or after its subject changed. For you this means webhook events can arrive more than once; de-duplicate on `svix-id`. - **Bounded contexts**: sending, feedback, integrations, marketing, templates and accounts each own their schema and talk through small interfaces, so any of them can be extracted. - **Read-your-writes**: `GET /emails/{id}` always reads the primary, even when lists are served by replicas. --- # API reference > Every Pidgely endpoint, generated from the OpenAPI 3.1 document, with parameters, request examples in six languages, responses and errors. Source: https://docs.pidgely.com/api-reference/ The Pidgely API is a JSON REST API at `https://api.pidgely.com`. Every page in this section is generated at build time from the [OpenAPI 3.1 document](https://docs.pidgely.com/openapi.yaml) (also as [JSON](https://docs.pidgely.com/openapi.json)), so it always matches the deployed contract. - **Authentication:** `Authorization: Bearer pdg_…` ([Authentication](https://docs.pidgely.com/authentication/)) - **Errors:** `{"statusCode", "name", "message"}` ([Errors](https://docs.pidgely.com/errors/)) - **Lists:** `limit`, `after`, `before` → `{"object": "list", "has_more", "data"}` ([Pagination](https://docs.pidgely.com/pagination/)) - **Retries:** `Idempotency-Key` on sends ([Idempotency](https://docs.pidgely.com/idempotency/)) - **Timestamps:** `YYYY-MM-DD HH:MM:SS.ffffff+00` in UTC Request examples show the call in each [official SDK](https://docs.pidgely.com/sdks/) (Node.js, Python, PHP, Go, Ruby; preview) and the raw request in cURL. The **native API** (`/v1/…`) predates the main routes and is documented for completeness (cURL examples only); prefer the main endpoints and the SDKs for new code. Dashboard (cookie session) and operator endpoints are internal and not covered here. ## Emails | Endpoint | Description | |---|---| | [`POST /emails`](https://docs.pidgely.com/api-reference/emails/send/) | Send an email | | [`POST /emails/batch`](https://docs.pidgely.com/api-reference/emails/send-batch/) | Send a batch of emails | | [`GET /emails`](https://docs.pidgely.com/api-reference/emails/list/) | List emails | | [`GET /emails/{id}`](https://docs.pidgely.com/api-reference/emails/retrieve/) | Retrieve an email | | [`PATCH /emails/{id}`](https://docs.pidgely.com/api-reference/emails/update/) | Reschedule an email | | [`POST /emails/{id}/cancel`](https://docs.pidgely.com/api-reference/emails/cancel/) | Cancel a scheduled email | | [`GET /emails/{email_id}/attachments`](https://docs.pidgely.com/api-reference/emails/list-attachments/) | List an email's attachments | | [`GET /emails/{email_id}/attachments/{id}`](https://docs.pidgely.com/api-reference/emails/retrieve-attachment/) | Retrieve an attachment | ## Domains | Endpoint | Description | |---|---| | [`POST /domains`](https://docs.pidgely.com/api-reference/domains/create/) | Create a domain | | [`GET /domains`](https://docs.pidgely.com/api-reference/domains/list/) | List domains | | [`GET /domains/{id}`](https://docs.pidgely.com/api-reference/domains/retrieve/) | Retrieve a domain | | [`PATCH /domains/{id}`](https://docs.pidgely.com/api-reference/domains/update/) | Update a domain | | [`DELETE /domains/{id}`](https://docs.pidgely.com/api-reference/domains/delete/) | Delete a domain | | [`POST /domains/{id}/verify`](https://docs.pidgely.com/api-reference/domains/verify/) | Verify a domain's DNS records | ## API keys | Endpoint | Description | |---|---| | [`POST /api-keys`](https://docs.pidgely.com/api-reference/api-keys/create/) | Create an API key | | [`GET /api-keys`](https://docs.pidgely.com/api-reference/api-keys/list/) | List API keys | | [`PATCH /api-keys/{id}`](https://docs.pidgely.com/api-reference/api-keys/update/) | Rename an API key | | [`DELETE /api-keys/{id}`](https://docs.pidgely.com/api-reference/api-keys/delete/) | Delete an API key | ## Webhooks | Endpoint | Description | |---|---| | [`POST /webhooks`](https://docs.pidgely.com/api-reference/webhooks/create/) | Create a webhook | | [`GET /webhooks`](https://docs.pidgely.com/api-reference/webhooks/list/) | List webhooks | | [`GET /webhooks/{id}`](https://docs.pidgely.com/api-reference/webhooks/retrieve/) | Retrieve a webhook | | [`PATCH /webhooks/{id}`](https://docs.pidgely.com/api-reference/webhooks/update/) | Update a webhook | | [`DELETE /webhooks/{id}`](https://docs.pidgely.com/api-reference/webhooks/delete/) | Delete a webhook | | [`POST /webhooks/{id}/signing-secret/rotate`](https://docs.pidgely.com/api-reference/webhooks/rotate-secret/) | Rotate a signing secret | ## Templates | Endpoint | Description | |---|---| | [`POST /templates`](https://docs.pidgely.com/api-reference/templates/create/) | Create a template | | [`GET /templates`](https://docs.pidgely.com/api-reference/templates/list/) | List templates | | [`GET /templates/{key}`](https://docs.pidgely.com/api-reference/templates/retrieve/) | Retrieve a template (draft) | | [`PATCH /templates/{key}`](https://docs.pidgely.com/api-reference/templates/update/) | Update a template draft | | [`DELETE /templates/{key}`](https://docs.pidgely.com/api-reference/templates/delete/) | Delete a template | | [`POST /templates/{key}/preview`](https://docs.pidgely.com/api-reference/templates/preview/) | Preview a template | | [`POST /templates/{key}/publish`](https://docs.pidgely.com/api-reference/templates/publish/) | Publish a template | | [`POST /templates/{key}/duplicate`](https://docs.pidgely.com/api-reference/templates/duplicate/) | Duplicate a template | ## Contacts | Endpoint | Description | |---|---| | [`POST /contacts`](https://docs.pidgely.com/api-reference/contacts/create/) | Create a contact | | [`GET /contacts`](https://docs.pidgely.com/api-reference/contacts/list/) | List contacts | | [`GET /contacts/{key}`](https://docs.pidgely.com/api-reference/contacts/retrieve/) | Retrieve a contact | | [`PATCH /contacts/{key}`](https://docs.pidgely.com/api-reference/contacts/update/) | Update a contact (unsubscribed syncs the suppression list) | | [`DELETE /contacts/{key}`](https://docs.pidgely.com/api-reference/contacts/delete/) | Delete a contact | | [`POST /contacts/batch`](https://docs.pidgely.com/api-reference/contacts/post-batch/) | Upsert contacts in batch | | [`GET /contacts/{key}/segments`](https://docs.pidgely.com/api-reference/contacts/list-segments/) | List a contact's segments | | [`POST /contacts/{key}/segments/{segment_id}`](https://docs.pidgely.com/api-reference/contacts/add-segment/) | Add a contact to a segment | | [`DELETE /contacts/{key}/segments/{segment_id}`](https://docs.pidgely.com/api-reference/contacts/remove-segment/) | Remove a contact from a segment | ## Segments | Endpoint | Description | |---|---| | [`POST /segments`](https://docs.pidgely.com/api-reference/segments/create/) | Create a segment | | [`GET /segments`](https://docs.pidgely.com/api-reference/segments/list/) | List segments | | [`GET /segments/{id}`](https://docs.pidgely.com/api-reference/segments/retrieve/) | Retrieve a segment | | [`PATCH /segments/{id}`](https://docs.pidgely.com/api-reference/segments/update/) | Rename a segment | | [`DELETE /segments/{id}`](https://docs.pidgely.com/api-reference/segments/delete/) | Delete a segment | | [`GET /segments/{id}/contacts`](https://docs.pidgely.com/api-reference/segments/list-contacts/) | List a segment's contacts | ## Broadcasts | Endpoint | Description | |---|---| | [`POST /broadcasts`](https://docs.pidgely.com/api-reference/broadcasts/create/) | Create a broadcast | | [`GET /broadcasts`](https://docs.pidgely.com/api-reference/broadcasts/list/) | List broadcasts | | [`GET /broadcasts/{id}`](https://docs.pidgely.com/api-reference/broadcasts/retrieve/) | Retrieve a broadcast | | [`PATCH /broadcasts/{id}`](https://docs.pidgely.com/api-reference/broadcasts/update/) | Update a draft broadcast | | [`DELETE /broadcasts/{id}`](https://docs.pidgely.com/api-reference/broadcasts/delete/) | Delete a broadcast | | [`POST /broadcasts/{id}/send`](https://docs.pidgely.com/api-reference/broadcasts/send/) | Send a broadcast | | [`POST /broadcasts/{id}/cancel`](https://docs.pidgely.com/api-reference/broadcasts/cancel/) | Cancel a broadcast | | [`POST /broadcasts/{id}/duplicate`](https://docs.pidgely.com/api-reference/broadcasts/duplicate/) | Duplicate a broadcast | ## Suppressions | Endpoint | Description | |---|---| | [`POST /suppressions`](https://docs.pidgely.com/api-reference/suppressions/create/) | Add a suppression | | [`GET /suppressions`](https://docs.pidgely.com/api-reference/suppressions/list/) | List suppressions | | [`GET /suppressions/{id}`](https://docs.pidgely.com/api-reference/suppressions/retrieve/) | Retrieve a suppression | | [`DELETE /suppressions/{id}`](https://docs.pidgely.com/api-reference/suppressions/delete/) | Remove a suppression | | [`POST /suppressions/batch/add`](https://docs.pidgely.com/api-reference/suppressions/batch-add/) | Add suppressions in batch | | [`POST /suppressions/batch/remove`](https://docs.pidgely.com/api-reference/suppressions/batch-remove/) | Remove suppressions in batch | ## Audiences (legacy) | Endpoint | Description | |---|---| | [`POST /audiences`](https://docs.pidgely.com/api-reference/audiences/create/) | Create an audience | | [`GET /audiences`](https://docs.pidgely.com/api-reference/audiences/list/) | List audiences | | [`GET /audiences/{id}`](https://docs.pidgely.com/api-reference/audiences/retrieve/) | Retrieve an audience | | [`DELETE /audiences/{id}`](https://docs.pidgely.com/api-reference/audiences/delete/) | Delete an audience | | [`GET /audiences/{id}/contacts`](https://docs.pidgely.com/api-reference/audiences/list-contacts/) | List an audience's contacts | | [`POST /audiences/{id}/contacts`](https://docs.pidgely.com/api-reference/audiences/add-contact/) | Create a contact in an audience | | [`GET /audiences/{id}/contacts/{key}`](https://docs.pidgely.com/api-reference/audiences/retrieve-contact/) | Retrieve a contact | | [`PATCH /audiences/{id}/contacts/{key}`](https://docs.pidgely.com/api-reference/audiences/update-contact/) | Update a contact | | [`DELETE /audiences/{id}/contacts/{key}`](https://docs.pidgely.com/api-reference/audiences/remove-contact/) | Delete a contact | ## Native API (v1) | Endpoint | Description | |---|---| | [`POST /v1/api-keys`](https://docs.pidgely.com/api-reference/v1/api-keys/create/) | Create an API key | | [`GET /v1/api-keys`](https://docs.pidgely.com/api-reference/v1/api-keys/list/) | List API keys | | [`DELETE /v1/api-keys/{id}`](https://docs.pidgely.com/api-reference/v1/api-keys/delete/) | Revoke an API key | | [`POST /v1/domains`](https://docs.pidgely.com/api-reference/v1/domains/create/) | Create a domain | | [`GET /v1/domains`](https://docs.pidgely.com/api-reference/v1/domains/list/) | List domains | | [`GET /v1/domains/{id}`](https://docs.pidgely.com/api-reference/v1/domains/retrieve/) | Retrieve a domain | | [`DELETE /v1/domains/{id}`](https://docs.pidgely.com/api-reference/v1/domains/delete/) | Delete a domain | | [`POST /v1/domains/{id}/verify`](https://docs.pidgely.com/api-reference/v1/domains/verify/) | Verify DNS now | | [`POST /v1/emails`](https://docs.pidgely.com/api-reference/v1/emails/send/) | Send an email | | [`POST /v1/emails/batch`](https://docs.pidgely.com/api-reference/v1/emails/send-batch/) | Send up to 100 emails atomically | | [`GET /v1/emails`](https://docs.pidgely.com/api-reference/v1/emails/list/) | List emails | | [`GET /v1/emails/{id}`](https://docs.pidgely.com/api-reference/v1/emails/retrieve/) | Retrieve an email with deliveries | | [`POST /v1/emails/{id}/cancel`](https://docs.pidgely.com/api-reference/v1/emails/cancel/) | Cancel a scheduled email | | [`POST /v1/suppressions`](https://docs.pidgely.com/api-reference/v1/suppressions/create/) | Add a suppression | | [`GET /v1/suppressions`](https://docs.pidgely.com/api-reference/v1/suppressions/list/) | List suppressions | | [`DELETE /v1/suppressions/{address}`](https://docs.pidgely.com/api-reference/v1/suppressions/delete/) | Remove a suppression | | [`GET /v1/usage`](https://docs.pidgely.com/api-reference/v1/usage/retrieve/) | Retrieve usage | | [`POST /v1/webhooks`](https://docs.pidgely.com/api-reference/v1/webhooks/create/) | Create a webhook (native event names) | | [`GET /v1/webhooks`](https://docs.pidgely.com/api-reference/v1/webhooks/list/) | List webhooks | | [`DELETE /v1/webhooks/{id}`](https://docs.pidgely.com/api-reference/v1/webhooks/delete/) | Delete a webhook | --- # Send an email > Queues one email for delivery and returns its id. The from domain must be verified and allowed for the API key. Up to 50 unique recipients across to, cc and bcc; a text part is generated from html… Source: https://docs.pidgely.com/api-reference/emails/send/ `POST https://api.pidgely.com/emails` Queues one email for delivery and returns its id. The `from` domain must be [verified](https://docs.pidgely.com/guides/domains/) and allowed for the API key. Up to 50 unique recipients across `to`, `cc` and `bcc`; a text part is generated from `html` when you omit `text`. `scheduled_at` accepts ISO 8601 or natural language such as `in 1 hour` or `tomorrow at 9am` (UTC), up to 30 days ahead. Attachments take base64 `content` or a remote `path`. Send an [`Idempotency-Key`](https://docs.pidgely.com/idempotency/) to retry safely. See [Sending email](https://docs.pidgely.com/guides/sending/) for every option. **Authorization:** `Bearer pdg_…`. Any API key (`full_access` or `sending_access`). ## Headers | Name | Type | Required | Description | |---|---|---|---| | `Idempotency-Key` | string | No | Replays the stored response for 24 hours; a different body gets 409. Up to 256 characters. | ## Body parameters JSON object. | Name | Type | Required | Description | |---|---|---|---| | `from` | string | No | `Name `; may come from the template. | | `to` | string \| string[] | Yes | Recipient address (`a@x.com` or `Name `) or a list of them. Up to 50 unique recipients across `to`, `cc` and `bcc`. | | `cc` | string \| string[] | No | Carbon-copy recipients. Count toward the 50-recipient limit. | | `bcc` | string \| string[] | No | Blind carbon-copy recipients. Count toward the 50-recipient limit. | | `reply_to` | string \| string[] | No | Reply-To address or list of addresses. | | `subject` | string | No | Subject line, up to 998 bytes. Required unless a template provides it. | | `html` | string | No | HTML body. `html` or `text` is required (unless you send with a template). | | `text` | string | No | Generated from html when omitted. | | `headers` | object (map of string) | No | Custom headers. Platform-managed headers (From, To, Subject, Message-ID, DKIM-Signature, Return-Path, Reply-To, …) are rejected; values cannot contain line breaks. | | `attachments` | object[] | No | Files to attach. All attachments share a 40 MiB budget (decoded). Executable types (`.exe`, `.js`, `.bat`, …) are blocked. | | `attachments[].filename` | string | No | File name shown to the recipient. Defaults to the last segment of `path`. | | `attachments[].content` | string | No | Base64 content. Use either content or path. | | `attachments[].path` | string | No | http(s) URL downloaded at send time (SSRF-guarded, 40MB budget, no redirects). | | `attachments[].content_type` | string | No | MIME type. Guessed from the file extension when omitted. | | `attachments[].content_id` | string | No | Makes the attachment inline (`cid:` in HTML). | | `tags` | object[] | No | Name/value pairs attached to the email and returned in webhooks. | | `tags[].name` | string | Yes | Display name. Pattern: `^[A-Za-z0-9_-]{1,256}$`. | | `tags[].value` | string | Yes | Pattern: `^[A-Za-z0-9_-]{1,256}$`. | | `scheduled_at` | string | No | ISO 8601 or natural language (`in 1 hour`, `tomorrow at 9am`); within 30 days. | | `template` | object | No | — | | `template.id` | string | Yes | Template id or alias (published version is used). | | `template.variables` | object (map of string \| number) | No | Values for the template variables: strings up to 2,000 characters or numbers. | | `topic_id` | string | No | Not supported yet; a non-empty value is rejected. | | `stream` | string | No | Pidgely extension: `transactional` (default) or `broadcast` (adds RFC 8058 unsubscribe). One of `transactional`, `broadcast`. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.emails.send({ from: 'Acme ', to: ['delivered@example.com'], subject: 'Welcome to Acme', html: '

Thanks for signing up!

', tags: [ { name: 'category', value: 'welcome', }, ], }, { idempotencyKey: 'welcome-user-123', }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Emails.send({ "from": "Acme ", "to": ["delivered@example.com"], "subject": "Welcome to Acme", "html": "

Thanks for signing up!

", "tags": [ { "name": "category", "value": "welcome", }, ], }, { "idempotency_key": "welcome-user-123", }) print(result) ``` ```php tab="PHP" emails->send([ 'from' => 'Acme ', 'to' => ['delivered@example.com'], 'subject' => 'Welcome to Acme', 'html' => '

Thanks for signing up!

', 'tags' => [ [ 'name' => 'category', 'value' => 'welcome', ], ], ], [ 'idempotency_key' => 'welcome-user-123', ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Emails.Send(ctx, &pidgely.SendEmailRequest{ From: "Acme ", To: []string{"delivered@example.com"}, Subject: "Welcome to Acme", Html: "

Thanks for signing up!

", Tags: []pidgely.Tag{ { Name: "category", Value: "welcome", }, }, }, pidgely.WithIdempotencyKey("welcome-user-123")) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Emails.send({ from: "Acme ", to: ["delivered@example.com"], subject: "Welcome to Acme", html: "

Thanks for signing up!

", tags: [ { name: "category", value: "welcome" } ] }, { idempotency_key: "welcome-user-123" }) puts result ``` ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/emails' \ -H "Authorization: Bearer $PIDGELY_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: welcome-user-123' \ -d '{ "from": "Acme ", "to": [ "delivered@example.com" ], "subject": "Welcome to Acme", "html": "

Thanks for signing up!

", "tags": [ { "name": "category", "value": "welcome" } ] }' ``` ## Response `200 OK` ```json { "id": "0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21" } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | Unique id (UUID). UUID. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | | 403 | `validation_error` | The `from` domain is not verified, the key is restricted to another domain, or sending is paused for the account. | | 409 | `invalid_idempotent_request` | The `Idempotency-Key` was already used with a different request body. | | 400 | `invalid_idempotency_key` | The `Idempotency-Key` is longer than 256 characters. | | 422 | `invalid_attachment` | An attachment has neither `content` nor `path`. | | 429 | `daily_quota_exceeded` | The daily sending quota is used up (UTC day). | | 429 | `monthly_quota_exceeded` | The monthly sending quota is used up. | --- # Send a batch of emails > Sends 1 to 100 emails in one request. Each item has the same shape as Send an email; attachments, scheduled_at and template are allowed. Source: https://docs.pidgely.com/api-reference/emails/send-batch/ `POST https://api.pidgely.com/emails/batch` Sends 1 to 100 emails in one request. Each item has the same shape as [Send an email](https://docs.pidgely.com/api-reference/emails/send/); attachments, `scheduled_at` and `template` are allowed. By default the batch is atomic (`x-batch-validation: strict`): the first invalid item fails the whole request and the error names it, for example `[3].to`. With `x-batch-validation: permissive`, valid items are queued and invalid ones are reported in `errors` with their index. A batch that would exceed the daily quota is rejected as a whole. **Authorization:** `Bearer pdg_…`. Any API key (`full_access` or `sending_access`). ## Headers | Name | Type | Required | Description | |---|---|---|---| | `Idempotency-Key` | string | No | Replays the stored response for 24 hours; a different body gets 409. Up to 256 characters. | | `x-batch-validation` | string | No | strict (default): all or nothing; permissive: valid items are sent, invalid ones reported in errors. One of `strict`, `permissive`. | ## Body parameters A JSON array of objects (1–100 items). Each item: | Name | Type | Required | Description | |---|---|---|---| | `from` | string | No | `Name `; may come from the template. | | `to` | string \| string[] | Yes | Recipient address (`a@x.com` or `Name `) or a list of them. Up to 50 unique recipients across `to`, `cc` and `bcc`. | | `cc` | string \| string[] | No | Carbon-copy recipients. Count toward the 50-recipient limit. | | `bcc` | string \| string[] | No | Blind carbon-copy recipients. Count toward the 50-recipient limit. | | `reply_to` | string \| string[] | No | Reply-To address or list of addresses. | | `subject` | string | No | Subject line, up to 998 bytes. Required unless a template provides it. | | `html` | string | No | HTML body. `html` or `text` is required (unless you send with a template). | | `text` | string | No | Generated from html when omitted. | | `headers` | object (map of string) | No | Custom headers. Platform-managed headers (From, To, Subject, Message-ID, DKIM-Signature, Return-Path, Reply-To, …) are rejected; values cannot contain line breaks. | | `attachments` | object[] | No | Files to attach. All attachments share a 40 MiB budget (decoded). Executable types (`.exe`, `.js`, `.bat`, …) are blocked. | | `attachments[].filename` | string | No | File name shown to the recipient. Defaults to the last segment of `path`. | | `attachments[].content` | string | No | Base64 content. Use either content or path. | | `attachments[].path` | string | No | http(s) URL downloaded at send time (SSRF-guarded, 40MB budget, no redirects). | | `attachments[].content_type` | string | No | MIME type. Guessed from the file extension when omitted. | | `attachments[].content_id` | string | No | Makes the attachment inline (`cid:` in HTML). | | `tags` | object[] | No | Name/value pairs attached to the email and returned in webhooks. | | `tags[].name` | string | Yes | Display name. Pattern: `^[A-Za-z0-9_-]{1,256}$`. | | `tags[].value` | string | Yes | Pattern: `^[A-Za-z0-9_-]{1,256}$`. | | `scheduled_at` | string | No | ISO 8601 or natural language (`in 1 hour`, `tomorrow at 9am`); within 30 days. | | `template` | object | No | — | | `template.id` | string | Yes | Template id or alias (published version is used). | | `template.variables` | object (map of string \| number) | No | Values for the template variables: strings up to 2,000 characters or numbers. | | `topic_id` | string | No | Not supported yet; a non-empty value is rejected. | | `stream` | string | No | Pidgely extension: `transactional` (default) or `broadcast` (adds RFC 8058 unsubscribe). One of `transactional`, `broadcast`. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.batch.send([ { from: 'Acme ', to: ['alice@example.com'], subject: 'Hi Alice', html: '

Welcome, Alice!

', }, { from: 'Acme ', to: ['bob@example.com'], subject: 'Hi Bob', html: '

Welcome, Bob!

', }, ], { batchValidation: 'permissive', }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Batch.send([ { "from": "Acme ", "to": ["alice@example.com"], "subject": "Hi Alice", "html": "

Welcome, Alice!

", }, { "from": "Acme ", "to": ["bob@example.com"], "subject": "Hi Bob", "html": "

Welcome, Bob!

", }, ], { "batch_validation": "permissive", }) print(result) ``` ```php tab="PHP" batch->send([ [ 'from' => 'Acme ', 'to' => ['alice@example.com'], 'subject' => 'Hi Alice', 'html' => '

Welcome, Alice!

', ], [ 'from' => 'Acme ', 'to' => ['bob@example.com'], 'subject' => 'Hi Bob', 'html' => '

Welcome, Bob!

', ], ], [ 'batch_validation' => 'permissive', ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Batch.Send(ctx, []pidgely.SendEmailRequest{ { From: "Acme ", To: []string{"alice@example.com"}, Subject: "Hi Alice", Html: "

Welcome, Alice!

", }, { From: "Acme ", To: []string{"bob@example.com"}, Subject: "Hi Bob", Html: "

Welcome, Bob!

", }, }, pidgely.WithBatchValidation(pidgely.BatchPermissive)) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Batch.send([ { from: "Acme ", to: ["alice@example.com"], subject: "Hi Alice", html: "

Welcome, Alice!

" }, { from: "Acme ", to: ["bob@example.com"], subject: "Hi Bob", html: "

Welcome, Bob!

" } ], { batch_validation: "permissive" }) puts result ``` ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/emails/batch' \ -H "Authorization: Bearer $PIDGELY_API_KEY" \ -H 'Content-Type: application/json' \ -H 'x-batch-validation: permissive' \ -d '[ { "from": "Acme ", "to": [ "alice@example.com" ], "subject": "Hi Alice", "html": "

Welcome, Alice!

" }, { "from": "Acme ", "to": [ "bob@example.com" ], "subject": "Hi Bob", "html": "

Welcome, Bob!

" } ]' ``` ## Response `200 OK` ```json { "data": [ { "id": "0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21" }, { "id": "0199d8a2-7a10-7d02-8b4e-51c2a9f0e7b3" } ], "errors": [] } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `data` | object[] | Yes | Items of this page. | | `data[].id` | string | Yes | Unique id (UUID). UUID. | | `errors` | object[] | No | — | | `errors[].index` | integer | No | — | | `errors[].message` | string | No | — | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | | 409 | `invalid_idempotent_request` | The `Idempotency-Key` was already used with a different request body. | --- # List emails > Lists sent emails, newest first, with cursor pagination. The default page size is 20 (maximum 100). Source: https://docs.pidgely.com/api-reference/emails/list/ `GET https://api.pidgely.com/emails` Lists sent emails, newest first, with [cursor pagination](https://docs.pidgely.com/pagination/). The default page size is 20 (maximum 100). **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `limit` | integer | No | Page size (1-100). Between 1 and 100. | | `after` | string | No | Id of the last item of the previous page (older items). UUID. | | `before` | string | No | Id of the first item of the current page (newer items). UUID. | | `status` | string | No | Comma-separated last_event values (queued, scheduled, delivered, delivery_delayed, bounced, complained, suppressed, failed, canceled). Pidgely extension. | | `to` | string | No | Recipient address (exact), or an end date when not an address. Pidgely extension. | | `q` | string | No | Substring of the subject or From. Pidgely extension. | | `from` | string | No | Start date (YYYY-MM-DD or RFC 3339). Alias: since. | | `since` | string | No | — | | `until` | string | No | End date (inclusive day, or RFC 3339 instant). | | `recipient` | string | No | Recipient address (alias of to). | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.emails.list({ limit: 20, }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Emails.list({ "limit": 20, }) print(result) ``` ```php tab="PHP" emails->list([ 'limit' => 20, ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Emails.List(ctx, &pidgely.ListParams{ Limit: 20, }) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Emails.list({ limit: 20 }) puts result ``` ```bash tab="cURL" curl -X GET 'https://api.pidgely.com/emails?limit=20' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "object": "list", "has_more": false, "data": [ { "object": "email", "id": "0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21", "message_id": "<0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21@acme.com>", "to": [ "delivered@example.com" ], "cc": [], "bcc": [], "reply_to": [], "from": "Acme ", "subject": "Welcome to Acme", "html": "

Thanks for signing up!

", "text": "Thanks for signing up!", "created_at": "2026-10-12 16:44:41.000000+00", "scheduled_at": null, "last_event": "delivered", "tags": [ { "name": "category", "value": "welcome" } ] } ] } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | Yes | Object type. One of `list`. | | `has_more` | boolean | Yes | Whether another page exists. | | `data` | object[] | Yes | Items of this page. | | `data[].object` | string | No | Object type. | | `data[].id` | string | No | Unique id (UUID). UUID. | | `data[].message_id` | string | No | Message-ID header of the email. | | `data[].to` | string[] | No | Recipient address (`a@x.com` or `Name `) or a list of them. Up to 50 unique recipients across `to`, `cc` and `bcc`. | | `data[].cc` | string[] | No | Carbon-copy recipients. Count toward the 50-recipient limit. | | `data[].bcc` | string[] | No | Blind carbon-copy recipients. Count toward the 50-recipient limit. | | `data[].reply_to` | string[] | No | Reply-To address or list of addresses. | | `data[].from` | string | No | — | | `data[].subject` | string | No | Subject line, up to 998 bytes. Required unless a template provides it. | | `data[].html` | string \| null | No | HTML body. `html` or `text` is required (unless you send with a template). | | `data[].text` | string \| null | No | — | | `data[].created_at` | string | No | Timestamp in UTC (`2026-10-12 16:44:41.000000+00`). | | `data[].scheduled_at` | string \| null | No | Timestamp in UTC, or null. | | `data[].last_event` | string | No | Most advanced status across the recipients. One of `queued`, `scheduled`, `sent`, `delivered`, `delivery_delayed`, `bounced`, `complained`, `suppressed`, `failed`, `canceled`. | | `data[].tags` | object[] | No | Name/value pairs attached to the email and returned in webhooks. | | `data[].tags[].name` | string | Yes | Display name. Pattern: `^[A-Za-z0-9_-]{1,256}$`. | | `data[].tags[].value` | string | Yes | Pattern: `^[A-Za-z0-9_-]{1,256}$`. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Retrieve an email > Retrieves one email with its bodies and last_event, the most advanced status across its recipients: queued, scheduled, delivery_delayed, canceled, suppressed, failed, delivered, bounced or… Source: https://docs.pidgely.com/api-reference/emails/retrieve/ `GET https://api.pidgely.com/emails/{id}` Retrieves one email with its bodies and `last_event`, the most advanced status across its recipients: `queued`, `scheduled`, `delivery_delayed`, `canceled`, `suppressed`, `failed`, `delivered`, `bounced` or `complained`. Reads come from the primary database, so the email is visible immediately after the send. A `sending_access` key can call this endpoint. **Authorization:** `Bearer pdg_…`. Any API key (`full_access` or `sending_access`). ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | Resource id UUID. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.emails.get('0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21'); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Emails.get("0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21") print(result) ``` ```php tab="PHP" emails->get('0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21'); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Emails.Get(ctx, "0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21") if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Emails.get("0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21") puts result ``` ```bash tab="cURL" curl -X GET 'https://api.pidgely.com/emails/0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "object": "email", "id": "0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21", "message_id": "<0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21@acme.com>", "to": [ "delivered@example.com" ], "cc": [], "bcc": [], "reply_to": [], "from": "Acme ", "subject": "Welcome to Acme", "html": "

Thanks for signing up!

", "text": "Thanks for signing up!", "created_at": "2026-10-12 16:44:41.000000+00", "scheduled_at": null, "last_event": "delivered", "tags": [ { "name": "category", "value": "welcome" } ] } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | No | Object type. | | `id` | string | No | Unique id (UUID). UUID. | | `message_id` | string | No | Message-ID header of the email. | | `to` | string[] | No | Recipient address (`a@x.com` or `Name `) or a list of them. Up to 50 unique recipients across `to`, `cc` and `bcc`. | | `cc` | string[] | No | Carbon-copy recipients. Count toward the 50-recipient limit. | | `bcc` | string[] | No | Blind carbon-copy recipients. Count toward the 50-recipient limit. | | `reply_to` | string[] | No | Reply-To address or list of addresses. | | `from` | string | No | — | | `subject` | string | No | Subject line, up to 998 bytes. Required unless a template provides it. | | `html` | string \| null | No | HTML body. `html` or `text` is required (unless you send with a template). | | `text` | string \| null | No | — | | `created_at` | string | No | Timestamp in UTC (`2026-10-12 16:44:41.000000+00`). | | `scheduled_at` | string \| null | No | Timestamp in UTC, or null. | | `last_event` | string | No | Most advanced status across the recipients. One of `queued`, `scheduled`, `sent`, `delivered`, `delivery_delayed`, `bounced`, `complained`, `suppressed`, `failed`, `canceled`. | | `tags` | object[] | No | Name/value pairs attached to the email and returned in webhooks. | | `tags[].name` | string | Yes | Display name. Pattern: `^[A-Za-z0-9_-]{1,256}$`. | | `tags[].value` | string | Yes | Pattern: `^[A-Za-z0-9_-]{1,256}$`. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Reschedule an email > Changes the scheduled_at of an email that has not been sent yet. The new time must be in the future and within 30 days; ISO 8601 and natural language are accepted. Source: https://docs.pidgely.com/api-reference/emails/update/ `PATCH https://api.pidgely.com/emails/{id}` Changes the `scheduled_at` of an email that has not been sent yet. The new time must be in the future and within 30 days; ISO 8601 and [natural language](https://docs.pidgely.com/guides/sending/#scheduling) are accepted. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | Resource id UUID. | ## Body parameters JSON object. | Name | Type | Required | Description | |---|---|---|---| | `scheduled_at` | string | Yes | ISO 8601 or natural language (`in 1 hour`, `tomorrow at 9am`, UTC), within 30 days. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.emails.update({ id: '0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21', scheduledAt: 'tomorrow at 9am', }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Emails.update({ "id": "0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21", "scheduled_at": "tomorrow at 9am", }) print(result) ``` ```php tab="PHP" emails->update([ 'id' => '0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21', 'scheduled_at' => 'tomorrow at 9am', ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Emails.Update(ctx, "0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21", &pidgely.UpdateEmailRequest{ ScheduledAt: "tomorrow at 9am", }) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Emails.update({ id: "0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21", scheduled_at: "tomorrow at 9am" }) puts result ``` ```bash tab="cURL" curl -X PATCH 'https://api.pidgely.com/emails/0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21' \ -H "Authorization: Bearer $PIDGELY_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "scheduled_at": "tomorrow at 9am" }' ``` ## Response `200 OK` ```json { "object": "email", "id": "0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21" } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | Yes | Object type. | | `id` | string | Yes | Unique id (UUID). UUID. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | | 409 | `not_reschedulable` | The email is not scheduled any more (already sent or canceled). | --- # Cancel a scheduled email > Cancels a scheduled email that has not been sent yet. Its recipients end as canceled. Source: https://docs.pidgely.com/api-reference/emails/cancel/ `POST https://api.pidgely.com/emails/{id}/cancel` Cancels a scheduled email that has not been sent yet. Its recipients end as `canceled`. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | Resource id UUID. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.emails.cancel('0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21'); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Emails.cancel("0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21") print(result) ``` ```php tab="PHP" emails->cancel('0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21'); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Emails.Cancel(ctx, "0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21") if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Emails.cancel("0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21") puts result ``` ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/emails/0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21/cancel' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "object": "email", "id": "0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21" } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | Yes | Object type. | | `id` | string | Yes | Unique id (UUID). UUID. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | | 409 | `not_cancelable` | The email is not scheduled any more (already sent or canceled). | --- # List an email's attachments > Lists the attachments of a sent email. Each item has a signed download_url that expires after one hour. Source: https://docs.pidgely.com/api-reference/emails/list-attachments/ `GET https://api.pidgely.com/emails/{email_id}/attachments` Lists the attachments of a sent email. Each item has a signed `download_url` that expires after one hour. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `email_id` | string | Yes | Email id UUID. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.emails.attachments.list({ emailId: '0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21', }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Emails.Attachments.list({ "email_id": "0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21", }) print(result) ``` ```php tab="PHP" emails->attachments->list([ 'email_id' => '0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21', ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Emails.Attachments.List(ctx, "0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21", nil) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Emails::Attachments.list({ email_id: "0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21" }) puts result ``` ```bash tab="cURL" curl -X GET 'https://api.pidgely.com/emails/0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21/attachments' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "object": "list", "has_more": false, "data": [ { "object": "attachment", "id": "0199d8a2-6f41-7a8e-b1c3-2d7e9f05a6c4", "filename": "invoice.pdf", "size": 48213, "content_type": "application/pdf", "content_disposition": "attachment", "content_id": null, "download_url": "https://api.pidgely.com/files/attachments/…", "expires_at": "2026-10-12T17:44:41Z" } ] } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | Yes | Object type. One of `list`. | | `has_more` | boolean | Yes | Whether another page exists. | | `data` | object[] | Yes | Items of this page. | | `data[].object` | string | No | Object type. | | `data[].id` | string | No | Unique id (UUID). UUID. | | `data[].filename` | string | No | — | | `data[].size` | integer | No | — | | `data[].content_type` | string | No | — | | `data[].content_disposition` | string | No | One of `attachment`, `inline`. | | `data[].content_id` | string \| null | No | — | | `data[].download_url` | string | No | Signed link, valid for 1 hour. | | `data[].expires_at` | string | No | — | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Retrieve an attachment > Retrieves one attachment of a sent email, with a signed download_url valid for one hour. Source: https://docs.pidgely.com/api-reference/emails/retrieve-attachment/ `GET https://api.pidgely.com/emails/{email_id}/attachments/{id}` Retrieves one attachment of a sent email, with a signed `download_url` valid for one hour. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `email_id` | string | Yes | Email id UUID. | | `id` | string | Yes | Attachment id UUID. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.emails.attachments.get({ emailId: '0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21', id: '0199d8a2-6f41-7a8e-b1c3-2d7e9f05a6c4', }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Emails.Attachments.get({ "email_id": "0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21", "id": "0199d8a2-6f41-7a8e-b1c3-2d7e9f05a6c4", }) print(result) ``` ```php tab="PHP" emails->attachments->get([ 'email_id' => '0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21', 'id' => '0199d8a2-6f41-7a8e-b1c3-2d7e9f05a6c4', ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Emails.Attachments.Get(ctx, "0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21", "0199d8a2-6f41-7a8e-b1c3-2d7e9f05a6c4") if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Emails::Attachments.get({ email_id: "0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21", id: "0199d8a2-6f41-7a8e-b1c3-2d7e9f05a6c4" }) puts result ``` ```bash tab="cURL" curl -X GET 'https://api.pidgely.com/emails/0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21/attachments/0199d8a2-6f41-7a8e-b1c3-2d7e9f05a6c4' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "object": "attachment", "id": "0199d8a2-6f41-7a8e-b1c3-2d7e9f05a6c4", "filename": "invoice.pdf", "size": 48213, "content_type": "application/pdf", "content_disposition": "attachment", "content_id": null, "download_url": "https://api.pidgely.com/files/attachments/…", "expires_at": "2026-10-12T17:44:41Z" } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | No | Object type. | | `id` | string | No | Unique id (UUID). UUID. | | `filename` | string | No | — | | `size` | integer | No | — | | `content_type` | string | No | — | | `content_disposition` | string | No | One of `attachment`, `inline`. | | `content_id` | string \| null | No | — | | `download_url` | string | No | Signed link, valid for 1 hour. | | `expires_at` | string | No | — | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Create a domain > Adds a sending domain and returns the DNS records to publish: DKIM (TXT), the return-path MX and SPF TXT on a subdomain (bounces by default, change it with custom_return_path), and an optional… Source: https://docs.pidgely.com/api-reference/domains/create/ `POST https://api.pidgely.com/domains` Adds a sending domain and returns the DNS records to publish: DKIM (`TXT`), the return-path `MX` and SPF `TXT` on a subdomain (`bounces` by default, change it with `custom_return_path`), and an optional DMARC `TXT`. Then call [Verify a domain](https://docs.pidgely.com/api-reference/domains/verify/), or wait for the automatic checks. A domain can be verified by only one account, and your plan sets how many domains you can add. Receiving and open/click tracking are not supported yet: `capabilities.receiving: "enabled"` is rejected and the tracking flags are always `false`. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Body parameters JSON object. | Name | Type | Required | Description | |---|---|---|---| | `name` | string | Yes | Display name. | | `region` | string | No | Informative; set by the platform. | | `custom_return_path` | string | No | Bounce subdomain label (default `bounces`). | | `tls` | string | No | `opportunistic` (default): TLS when the receiving server offers it. `enforced`: deliver only over TLS. One of `opportunistic`, `enforced`. | | `open_tracking` | boolean | No | Accepted, not supported yet (always false). | | `click_tracking` | boolean | No | Accepted, not supported yet. | | `capabilities` | object | No | — | | `capabilities.sending` | string | No | — | | `capabilities.receiving` | string | No | `enabled` is rejected (receiving not supported yet). | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.domains.create({ name: 'acme.com', customReturnPath: 'bounces', tls: 'opportunistic', }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Domains.create({ "name": "acme.com", "custom_return_path": "bounces", "tls": "opportunistic", }) print(result) ``` ```php tab="PHP" domains->create([ 'name' => 'acme.com', 'custom_return_path' => 'bounces', 'tls' => 'opportunistic', ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Domains.Create(ctx, &pidgely.CreateDomainRequest{ Name: "acme.com", CustomReturnPath: "bounces", Tls: "opportunistic", }) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Domains.create({ name: "acme.com", custom_return_path: "bounces", tls: "opportunistic" }) puts result ``` ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/domains' \ -H "Authorization: Bearer $PIDGELY_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "name": "acme.com", "custom_return_path": "bounces", "tls": "opportunistic" }' ``` ## Response `200 OK` ```json { "object": "domain", "id": "0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51", "name": "acme.com", "status": "not_started", "created_at": "2026-10-12 16:44:41.000000+00", "region": "sa-east-1", "open_tracking": false, "click_tracking": false, "tls": "opportunistic", "capabilities": { "sending": "enabled", "receiving": "disabled" }, "records": [ { "record": "DKIM", "name": "pdg202610._domainkey", "type": "TXT", "ttl": "Auto", "status": "not_started", "value": "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA…" }, { "record": "SPF", "name": "bounces", "type": "MX", "ttl": "Auto", "status": "not_started", "value": "mta.pidgely.com", "priority": 10 }, { "record": "SPF", "name": "bounces", "type": "TXT", "ttl": "Auto", "status": "not_started", "value": "v=spf1 include:_spf.mta.pidgely.com ~all" }, { "record": "DMARC", "name": "_dmarc", "type": "TXT", "ttl": "Auto", "status": "not_started", "value": "v=DMARC1; p=none;" } ] } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | No | Object type. | | `id` | string | No | Unique id (UUID). UUID. | | `name` | string | No | Display name. | | `status` | string | No | Current status. One of `not_started`, `pending`, `verified`, `failed`. | | `created_at` | string | No | Timestamp in UTC (`2026-10-12 16:44:41.000000+00`). | | `region` | string | No | Informative; set by the platform. | | `open_tracking` | boolean | No | — | | `click_tracking` | boolean | No | — | | `tls` | string | No | `opportunistic` (default): TLS when the receiving server offers it. `enforced`: deliver only over TLS. One of `opportunistic`, `enforced`. | | `capabilities` | object | No | — | | `capabilities.sending` | string | No | — | | `capabilities.receiving` | string | No | — | | `records` | object[] | No | DNS records to publish, with their verification status. | | `records[].record` | string | No | One of `DKIM`, `SPF`, `DMARC`. | | `records[].name` | string | No | Relative to the domain. | | `records[].type` | string | No | One of `TXT`, `MX`. | | `records[].ttl` | string | No | — | | `records[].status` | string | No | Current status. | | `records[].value` | string | No | Record value. | | `records[].priority` | integer | No | MX priority. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | | 403 | `validation_error` | The domain already exists in this account, or the plan limit on domains is reached. | --- # List domains > Lists the domains of the account, newest first. Source: https://docs.pidgely.com/api-reference/domains/list/ `GET https://api.pidgely.com/domains` Lists the domains of the account, newest first. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `limit` | integer | No | Page size (1-100). Between 1 and 100. | | `after` | string | No | Id of the last item of the previous page (older items). UUID. | | `before` | string | No | Id of the first item of the current page (newer items). UUID. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.domains.list(); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Domains.list() print(result) ``` ```php tab="PHP" domains->list(); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Domains.List(ctx, nil) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Domains.list() puts result ``` ```bash tab="cURL" curl -X GET 'https://api.pidgely.com/domains' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "object": "list", "has_more": false, "data": [ { "object": "domain", "id": "0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51", "name": "acme.com", "status": "verified", "created_at": "2026-10-12 16:44:41.000000+00", "region": "sa-east-1", "open_tracking": false, "click_tracking": false, "tls": "opportunistic", "capabilities": { "sending": "enabled", "receiving": "disabled" } } ] } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | Yes | Object type. One of `list`. | | `has_more` | boolean | Yes | Whether another page exists. | | `data` | object[] | Yes | Items of this page. | | `data[].object` | string | No | Object type. | | `data[].id` | string | No | Unique id (UUID). UUID. | | `data[].name` | string | No | Display name. | | `data[].status` | string | No | Current status. One of `not_started`, `pending`, `verified`, `failed`. | | `data[].created_at` | string | No | Timestamp in UTC (`2026-10-12 16:44:41.000000+00`). | | `data[].region` | string | No | Informative; set by the platform. | | `data[].open_tracking` | boolean | No | — | | `data[].click_tracking` | boolean | No | — | | `data[].tls` | string | No | `opportunistic` (default): TLS when the receiving server offers it. `enforced`: deliver only over TLS. One of `opportunistic`, `enforced`. | | `data[].capabilities` | object | No | — | | `data[].capabilities.sending` | string | No | — | | `data[].capabilities.receiving` | string | No | — | | `data[].records` | object[] | No | DNS records to publish, with their verification status. | | `data[].records[].record` | string | No | One of `DKIM`, `SPF`, `DMARC`. | | `data[].records[].name` | string | No | Relative to the domain. | | `data[].records[].type` | string | No | One of `TXT`, `MX`. | | `data[].records[].ttl` | string | No | — | | `data[].records[].status` | string | No | Current status. | | `data[].records[].value` | string | No | Record value. | | `data[].records[].priority` | integer | No | MX priority. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Retrieve a domain > Retrieves a domain with its DNS records and the status of each one (not_started, pending, verified or failed). Source: https://docs.pidgely.com/api-reference/domains/retrieve/ `GET https://api.pidgely.com/domains/{id}` Retrieves a domain with its DNS records and the status of each one (`not_started`, `pending`, `verified` or `failed`). **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | Resource id UUID. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.domains.get('0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51'); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Domains.get("0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51") print(result) ``` ```php tab="PHP" domains->get('0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51'); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Domains.Get(ctx, "0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51") if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Domains.get("0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51") puts result ``` ```bash tab="cURL" curl -X GET 'https://api.pidgely.com/domains/0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "object": "domain", "id": "0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51", "name": "acme.com", "status": "verified", "created_at": "2026-10-12 16:44:41.000000+00", "region": "sa-east-1", "open_tracking": false, "click_tracking": false, "tls": "opportunistic", "capabilities": { "sending": "enabled", "receiving": "disabled" }, "records": [ { "record": "DKIM", "name": "pdg202610._domainkey", "type": "TXT", "ttl": "Auto", "status": "verified", "value": "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA…" }, { "record": "SPF", "name": "bounces", "type": "MX", "ttl": "Auto", "status": "verified", "value": "mta.pidgely.com", "priority": 10 }, { "record": "SPF", "name": "bounces", "type": "TXT", "ttl": "Auto", "status": "verified", "value": "v=spf1 include:_spf.mta.pidgely.com ~all" }, { "record": "DMARC", "name": "_dmarc", "type": "TXT", "ttl": "Auto", "status": "verified", "value": "v=DMARC1; p=none;" } ] } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | No | Object type. | | `id` | string | No | Unique id (UUID). UUID. | | `name` | string | No | Display name. | | `status` | string | No | Current status. One of `not_started`, `pending`, `verified`, `failed`. | | `created_at` | string | No | Timestamp in UTC (`2026-10-12 16:44:41.000000+00`). | | `region` | string | No | Informative; set by the platform. | | `open_tracking` | boolean | No | — | | `click_tracking` | boolean | No | — | | `tls` | string | No | `opportunistic` (default): TLS when the receiving server offers it. `enforced`: deliver only over TLS. One of `opportunistic`, `enforced`. | | `capabilities` | object | No | — | | `capabilities.sending` | string | No | — | | `capabilities.receiving` | string | No | — | | `records` | object[] | No | DNS records to publish, with their verification status. | | `records[].record` | string | No | One of `DKIM`, `SPF`, `DMARC`. | | `records[].name` | string | No | Relative to the domain. | | `records[].type` | string | No | One of `TXT`, `MX`. | | `records[].ttl` | string | No | — | | `records[].status` | string | No | Current status. | | `records[].value` | string | No | Record value. | | `records[].priority` | integer | No | MX priority. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Update a domain > Updates the TLS policy of a domain. enforced makes the MTA require TLS when delivering mail from this domain; opportunistic (the default) uses TLS whenever the receiving server offers it. Open and… Source: https://docs.pidgely.com/api-reference/domains/update/ `PATCH https://api.pidgely.com/domains/{id}` Updates the TLS policy of a domain. `enforced` makes the MTA require TLS when delivering mail from this domain; `opportunistic` (the default) uses TLS whenever the receiving server offers it. Open and click tracking are not supported yet. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | Resource id UUID. | ## Body parameters JSON object. | Name | Type | Required | Description | |---|---|---|---| | `tls` | string | No | `opportunistic` (default): TLS when the receiving server offers it. `enforced`: deliver only over TLS. One of `opportunistic`, `enforced`. | | `open_tracking` | boolean | No | — | | `click_tracking` | boolean | No | — | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.domains.update({ id: '0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51', tls: 'enforced', }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Domains.update({ "id": "0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51", "tls": "enforced", }) print(result) ``` ```php tab="PHP" domains->update([ 'id' => '0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51', 'tls' => 'enforced', ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Domains.Update(ctx, "0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51", &pidgely.UpdateDomainRequest{ Tls: "enforced", }) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Domains.update({ id: "0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51", tls: "enforced" }) puts result ``` ```bash tab="cURL" curl -X PATCH 'https://api.pidgely.com/domains/0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51' \ -H "Authorization: Bearer $PIDGELY_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "tls": "enforced" }' ``` ## Response `200 OK` ```json { "object": "domain", "id": "0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51" } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | Yes | Object type. | | `id` | string | Yes | Unique id (UUID). UUID. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Delete a domain > Deletes a domain. Mail from it can no longer be sent; messages still queued for it are deferred and eventually fail. Source: https://docs.pidgely.com/api-reference/domains/delete/ `DELETE https://api.pidgely.com/domains/{id}` Deletes a domain. Mail from it can no longer be sent; messages still queued for it are deferred and eventually fail. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | Resource id UUID. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.domains.remove('0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51'); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Domains.remove("0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51") print(result) ``` ```php tab="PHP" domains->remove('0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51'); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Domains.Remove(ctx, "0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51") if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Domains.remove("0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51") puts result ``` ```bash tab="cURL" curl -X DELETE 'https://api.pidgely.com/domains/0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "object": "domain", "id": "0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51", "deleted": true } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | Yes | Object type. | | `id` | string | Yes | Unique id (UUID). UUID. | | `deleted` | boolean | Yes | — | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Verify a domain's DNS records > Checks the DNS records now instead of waiting for the periodic check (every 5 minutes for new domains during their first 3 days, then daily). Sending is allowed once DKIM, the return-path MX and… Source: https://docs.pidgely.com/api-reference/domains/verify/ `POST https://api.pidgely.com/domains/{id}/verify` Checks the DNS records now instead of waiting for the periodic check (every 5 minutes for new domains during their first 3 days, then daily). Sending is allowed once DKIM, the return-path MX and SPF pass; DMARC is recommended but not required. Watch `domain.updated` webhooks or retrieve the domain to see the result. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | Resource id UUID. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.domains.verify('0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51'); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Domains.verify("0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51") print(result) ``` ```php tab="PHP" domains->verify('0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51'); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Domains.Verify(ctx, "0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51") if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Domains.verify("0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51") puts result ``` ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/domains/0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51/verify' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "object": "domain", "id": "0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51" } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | Yes | Object type. | | `id` | string | Yes | Unique id (UUID). UUID. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Create an API key > Creates an API key. The token (pdg__) is returned only once; Pidgely stores a hash. full_access (default) can call every endpoint; sending_access can only send and retrieve emails.… Source: https://docs.pidgely.com/api-reference/api-keys/create/ `POST https://api.pidgely.com/api-keys` Creates an API key. The token (`pdg__`) is returned **only once**; Pidgely stores a hash. `full_access` (default) can call every endpoint; `sending_access` can only send and retrieve emails. Passing `domain_id` creates a `sending_access` key that can send only from that domain. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Body parameters JSON object. | Name | Type | Required | Description | |---|---|---|---| | `name` | string | Yes | Display name. | | `permission` | string | No | `full_access` (default) or `sending_access`. One of `full_access`, `sending_access`. | | `domain_id` | string | No | Restricts the key to sending from this domain (implies `sending_access`). UUID. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.apiKeys.create({ name: 'Production', permission: 'sending_access', domainId: '0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51', }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.ApiKeys.create({ "name": "Production", "permission": "sending_access", "domain_id": "0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51", }) print(result) ``` ```php tab="PHP" apiKeys->create([ 'name' => 'Production', 'permission' => 'sending_access', 'domain_id' => '0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51', ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.ApiKeys.Create(ctx, &pidgely.CreateApiKeyRequest{ Name: "Production", Permission: "sending_access", DomainId: "0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51", }) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::ApiKeys.create({ name: "Production", permission: "sending_access", domain_id: "0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51" }) puts result ``` ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/api-keys' \ -H "Authorization: Bearer $PIDGELY_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "name": "Production", "permission": "sending_access", "domain_id": "0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51" }' ``` ## Response `200 OK` ```json { "id": "0199d7f1-3a5c-7b2e-9d4f-6e1a0c8b7d92", "object": "api_key", "token": "pdg_k3x7m2qa4bnp6c5d_xyy4u7c2r3w5t6v7m8n9p2q3r4s5t6u7" } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | No | Object type. | | `id` | string | No | Unique id (UUID). UUID. | | `token` | string | No | Shown once: `pdg__`. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # List API keys > Lists API keys. Tokens are never returned again. Source: https://docs.pidgely.com/api-reference/api-keys/list/ `GET https://api.pidgely.com/api-keys` Lists API keys. Tokens are never returned again. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `limit` | integer | No | Page size (1-100). Between 1 and 100. | | `after` | string | No | Id of the last item of the previous page (older items). UUID. | | `before` | string | No | Id of the first item of the current page (newer items). UUID. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.apiKeys.list(); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.ApiKeys.list() print(result) ``` ```php tab="PHP" apiKeys->list(); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.ApiKeys.List(ctx, nil) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::ApiKeys.list() puts result ``` ```bash tab="cURL" curl -X GET 'https://api.pidgely.com/api-keys' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "object": "list", "has_more": false, "data": [ { "id": "0199d7f1-3a5c-7b2e-9d4f-6e1a0c8b7d92", "name": "Production", "permission": "sending_access", "created_at": "2026-10-12 16:44:41.000000+00", "last_used_at": "2026-10-12 16:44:41.000000+00" } ] } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | Yes | Object type. One of `list`. | | `has_more` | boolean | Yes | Whether another page exists. | | `data` | object[] | Yes | Items of this page. | | `data[].id` | string | No | Unique id (UUID). UUID. | | `data[].name` | string | No | Display name. | | `data[].permission` | string | No | `full_access` (default) or `sending_access`. One of `full_access`, `sending_access`. | | `data[].created_at` | string | No | Timestamp in UTC (`2026-10-12 16:44:41.000000+00`). | | `data[].last_used_at` | string \| null | No | Timestamp in UTC, or null. | | `data[].prefix` | string | No | Public token prefix (`pdg_`), to recognise a key. | | `data[].domain_id` | string \| null | No | Domain the key is restricted to, if any. UUID. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Rename an API key > Renames an API key. Permissions cannot be changed: create a new key instead. Source: https://docs.pidgely.com/api-reference/api-keys/update/ `PATCH https://api.pidgely.com/api-keys/{id}` Renames an API key. Permissions cannot be changed: create a new key instead. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | Resource id UUID. | ## Body parameters JSON object. | Name | Type | Required | Description | |---|---|---|---| | `name` | string | Yes | Display name. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.apiKeys.update('0199d7f1-3a5c-7b2e-9d4f-6e1a0c8b7d92', { name: 'Production (web)', }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.ApiKeys.update("0199d7f1-3a5c-7b2e-9d4f-6e1a0c8b7d92", { "name": "Production (web)", }) print(result) ``` ```php tab="PHP" apiKeys->update('0199d7f1-3a5c-7b2e-9d4f-6e1a0c8b7d92', [ 'name' => 'Production (web)', ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.ApiKeys.Update(ctx, "0199d7f1-3a5c-7b2e-9d4f-6e1a0c8b7d92", &pidgely.UpdateApiKeyRequest{ Name: "Production (web)", }) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::ApiKeys.update("0199d7f1-3a5c-7b2e-9d4f-6e1a0c8b7d92", { name: "Production (web)" }) puts result ``` ```bash tab="cURL" curl -X PATCH 'https://api.pidgely.com/api-keys/0199d7f1-3a5c-7b2e-9d4f-6e1a0c8b7d92' \ -H "Authorization: Bearer $PIDGELY_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "name": "Production (web)" }' ``` ## Response `200 OK` ```json { "object": "api_key", "id": "0199d7f1-3a5c-7b2e-9d4f-6e1a0c8b7d92" } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | Yes | Object type. | | `id` | string | Yes | Unique id (UUID). UUID. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Delete an API key > Revokes an API key. Requests that use it are rejected with invalid_api_key within about 30 seconds (authentication results are cached briefly). Source: https://docs.pidgely.com/api-reference/api-keys/delete/ `DELETE https://api.pidgely.com/api-keys/{id}` Revokes an API key. Requests that use it are rejected with `invalid_api_key` within about 30 seconds (authentication results are cached briefly). **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | Resource id UUID. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.apiKeys.remove('0199d7f1-3a5c-7b2e-9d4f-6e1a0c8b7d92'); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.ApiKeys.remove("0199d7f1-3a5c-7b2e-9d4f-6e1a0c8b7d92") print(result) ``` ```php tab="PHP" apiKeys->remove('0199d7f1-3a5c-7b2e-9d4f-6e1a0c8b7d92'); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.ApiKeys.Remove(ctx, "0199d7f1-3a5c-7b2e-9d4f-6e1a0c8b7d92") if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::ApiKeys.remove("0199d7f1-3a5c-7b2e-9d4f-6e1a0c8b7d92") puts result ``` ```bash tab="cURL" curl -X DELETE 'https://api.pidgely.com/api-keys/0199d7f1-3a5c-7b2e-9d4f-6e1a0c8b7d92' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "object": "api_key", "id": "0199d7f1-3a5c-7b2e-9d4f-6e1a0c8b7d92", "deleted": true } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | Yes | Object type. | | `id` | string | Yes | Unique id (UUID). UUID. | | `deleted` | boolean | Yes | — | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Create a webhook > Registers an HTTPS endpoint for one or more events. Deliveries are signed with the Standard Webhooks scheme (webhook- and svix- headers). The response includes the signing_secret (whsec_…) used to… Source: https://docs.pidgely.com/api-reference/webhooks/create/ `POST https://api.pidgely.com/webhooks` Registers an HTTPS endpoint for one or more events. Deliveries are signed with the Standard Webhooks scheme (`webhook-*` and `svix-*` headers). The response includes the `signing_secret` (`whsec_…`) used to [verify signatures](https://docs.pidgely.com/guides/webhooks/#verify-signatures). The endpoint must be a public `https` URL. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Body parameters JSON object. | Name | Type | Required | Description | |---|---|---|---| | `endpoint` | string | Yes | Public `https` URL that receives the events. | | `events` | string[] | Yes | Event types to subscribe to, for example `email.delivered`. See [Webhooks](https://docs.pidgely.com/guides/webhooks/#events). | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.webhooks.create({ endpoint: 'https://acme.com/webhooks/pidgely', events: ['email.delivered', 'email.bounced', 'email.complained'], }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Webhooks.create({ "endpoint": "https://acme.com/webhooks/pidgely", "events": ["email.delivered", "email.bounced", "email.complained"], }) print(result) ``` ```php tab="PHP" webhooks->create([ 'endpoint' => 'https://acme.com/webhooks/pidgely', 'events' => ['email.delivered', 'email.bounced', 'email.complained'], ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Webhooks.Create(ctx, &pidgely.CreateWebhookRequest{ Endpoint: "https://acme.com/webhooks/pidgely", Events: []string{"email.delivered", "email.bounced", "email.complained"}, }) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Webhooks.create({ endpoint: "https://acme.com/webhooks/pidgely", events: ["email.delivered", "email.bounced", "email.complained"] }) puts result ``` ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/webhooks' \ -H "Authorization: Bearer $PIDGELY_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "endpoint": "https://acme.com/webhooks/pidgely", "events": [ "email.delivered", "email.bounced", "email.complained" ] }' ``` ## Response `200 OK` ```json { "object": "webhook", "id": "0199d7f2-8b1d-7c3a-a2e5-4f9c6d0b1e73", "signing_secret": "whsec_9rXbB2n3cKqL1sT8vW4yZ6aD0fG5hJ7k" } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | No | Object type. | | `id` | string | No | Unique id (UUID). UUID. | | `signing_secret` | string | No | Secret (`whsec_…`) used to verify webhook signatures. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # List webhooks > Lists webhook endpoints. Source: https://docs.pidgely.com/api-reference/webhooks/list/ `GET https://api.pidgely.com/webhooks` Lists webhook endpoints. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `limit` | integer | No | Page size (1-100). Between 1 and 100. | | `after` | string | No | Id of the last item of the previous page (older items). UUID. | | `before` | string | No | Id of the first item of the current page (newer items). UUID. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.webhooks.list(); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Webhooks.list() print(result) ``` ```php tab="PHP" webhooks->list(); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Webhooks.List(ctx, nil) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Webhooks.list() puts result ``` ```bash tab="cURL" curl -X GET 'https://api.pidgely.com/webhooks' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "object": "list", "has_more": false, "data": [ { "object": "webhook", "id": "0199d7f2-8b1d-7c3a-a2e5-4f9c6d0b1e73", "created_at": "2026-10-12 16:44:41.000000+00", "status": "enabled", "endpoint": "https://acme.com/webhooks/pidgely", "events": [ "email.delivered", "email.bounced" ] } ] } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | Yes | Object type. One of `list`. | | `has_more` | boolean | Yes | Whether another page exists. | | `data` | object[] | Yes | Items of this page. | | `data[].object` | string | No | Object type. | | `data[].id` | string | No | Unique id (UUID). UUID. | | `data[].created_at` | string | No | Timestamp in UTC (`2026-10-12 16:44:41.000000+00`). | | `data[].status` | string | No | Current status. One of `enabled`, `disabled`. | | `data[].endpoint` | string | No | Public `https` URL that receives the events. | | `data[].events` | string[] | No | Event types to subscribe to, for example `email.delivered`. See [Webhooks](https://docs.pidgely.com/guides/webhooks/#events). | | `data[].signing_secret` | string | No | Secret (`whsec_…`) used to verify webhook signatures. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Retrieve a webhook > Retrieves a webhook endpoint, including its signing secret. Source: https://docs.pidgely.com/api-reference/webhooks/retrieve/ `GET https://api.pidgely.com/webhooks/{id}` Retrieves a webhook endpoint, including its signing secret. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | Resource id UUID. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.webhooks.get('0199d7f2-8b1d-7c3a-a2e5-4f9c6d0b1e73'); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Webhooks.get("0199d7f2-8b1d-7c3a-a2e5-4f9c6d0b1e73") print(result) ``` ```php tab="PHP" webhooks->get('0199d7f2-8b1d-7c3a-a2e5-4f9c6d0b1e73'); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Webhooks.Get(ctx, "0199d7f2-8b1d-7c3a-a2e5-4f9c6d0b1e73") if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Webhooks.get("0199d7f2-8b1d-7c3a-a2e5-4f9c6d0b1e73") puts result ``` ```bash tab="cURL" curl -X GET 'https://api.pidgely.com/webhooks/0199d7f2-8b1d-7c3a-a2e5-4f9c6d0b1e73' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "object": "webhook", "id": "0199d7f2-8b1d-7c3a-a2e5-4f9c6d0b1e73", "created_at": "2026-10-12 16:44:41.000000+00", "status": "enabled", "endpoint": "https://acme.com/webhooks/pidgely", "events": [ "email.delivered", "email.bounced" ], "signing_secret": "whsec_9rXbB2n3cKqL1sT8vW4yZ6aD0fG5hJ7k" } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | No | Object type. | | `id` | string | No | Unique id (UUID). UUID. | | `created_at` | string | No | Timestamp in UTC (`2026-10-12 16:44:41.000000+00`). | | `status` | string | No | Current status. One of `enabled`, `disabled`. | | `endpoint` | string | No | Public `https` URL that receives the events. | | `events` | string[] | No | Event types to subscribe to, for example `email.delivered`. See [Webhooks](https://docs.pidgely.com/guides/webhooks/#events). | | `signing_secret` | string | No | Secret (`whsec_…`) used to verify webhook signatures. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Update a webhook > Changes the endpoint URL, the subscribed events or the status. A disabled webhook receives nothing, and deliveries already queued for it are dropped. Source: https://docs.pidgely.com/api-reference/webhooks/update/ `PATCH https://api.pidgely.com/webhooks/{id}` Changes the endpoint URL, the subscribed events or the status. A `disabled` webhook receives nothing, and deliveries already queued for it are dropped. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | Resource id UUID. | ## Body parameters JSON object. | Name | Type | Required | Description | |---|---|---|---| | `endpoint` | string | No | Public `https` URL that receives the events. | | `events` | string[] | No | Event types to subscribe to, for example `email.delivered`. See [Webhooks](https://docs.pidgely.com/guides/webhooks/#events). | | `status` | string | No | Current status. One of `enabled`, `disabled`. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.webhooks.update('0199d7f2-8b1d-7c3a-a2e5-4f9c6d0b1e73', { events: [ 'email.delivered', 'email.bounced', 'email.complained', 'email.delivery_delayed', ], status: 'enabled', }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Webhooks.update("0199d7f2-8b1d-7c3a-a2e5-4f9c6d0b1e73", { "events": [ "email.delivered", "email.bounced", "email.complained", "email.delivery_delayed", ], "status": "enabled", }) print(result) ``` ```php tab="PHP" webhooks->update('0199d7f2-8b1d-7c3a-a2e5-4f9c6d0b1e73', [ 'events' => [ 'email.delivered', 'email.bounced', 'email.complained', 'email.delivery_delayed', ], 'status' => 'enabled', ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Webhooks.Update(ctx, "0199d7f2-8b1d-7c3a-a2e5-4f9c6d0b1e73", &pidgely.UpdateWebhookRequest{ Events: []string{"email.delivered", "email.bounced", "email.complained", "email.delivery_delayed"}, Status: "enabled", }) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Webhooks.update("0199d7f2-8b1d-7c3a-a2e5-4f9c6d0b1e73", { events: [ "email.delivered", "email.bounced", "email.complained", "email.delivery_delayed" ], status: "enabled" }) puts result ``` ```bash tab="cURL" curl -X PATCH 'https://api.pidgely.com/webhooks/0199d7f2-8b1d-7c3a-a2e5-4f9c6d0b1e73' \ -H "Authorization: Bearer $PIDGELY_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "events": [ "email.delivered", "email.bounced", "email.complained", "email.delivery_delayed" ], "status": "enabled" }' ``` ## Response `200 OK` ```json { "object": "webhook", "id": "0199d7f2-8b1d-7c3a-a2e5-4f9c6d0b1e73" } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | Yes | Object type. | | `id` | string | Yes | Unique id (UUID). UUID. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Delete a webhook > Deletes a webhook endpoint. Queued deliveries for it are dropped. Source: https://docs.pidgely.com/api-reference/webhooks/delete/ `DELETE https://api.pidgely.com/webhooks/{id}` Deletes a webhook endpoint. Queued deliveries for it are dropped. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | Resource id UUID. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.webhooks.remove('0199d7f2-8b1d-7c3a-a2e5-4f9c6d0b1e73'); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Webhooks.remove("0199d7f2-8b1d-7c3a-a2e5-4f9c6d0b1e73") print(result) ``` ```php tab="PHP" webhooks->remove('0199d7f2-8b1d-7c3a-a2e5-4f9c6d0b1e73'); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Webhooks.Remove(ctx, "0199d7f2-8b1d-7c3a-a2e5-4f9c6d0b1e73") if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Webhooks.remove("0199d7f2-8b1d-7c3a-a2e5-4f9c6d0b1e73") puts result ``` ```bash tab="cURL" curl -X DELETE 'https://api.pidgely.com/webhooks/0199d7f2-8b1d-7c3a-a2e5-4f9c6d0b1e73' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "object": "webhook", "id": "0199d7f2-8b1d-7c3a-a2e5-4f9c6d0b1e73", "deleted": true } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | Yes | Object type. | | `id` | string | Yes | Unique id (UUID). UUID. | | `deleted` | boolean | Yes | — | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Rotate a signing secret > Generates a new signing secret and returns it. The old secret stops working immediately, including for retries already queued, so deploy the new secret to your receiver right after rotating. Source: https://docs.pidgely.com/api-reference/webhooks/rotate-secret/ `POST https://api.pidgely.com/webhooks/{id}/signing-secret/rotate` Generates a new signing secret and returns it. The old secret stops working **immediately**, including for retries already queued, so deploy the new secret to your receiver right after rotating. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | Resource id UUID. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.webhooks.rotateSigningSecret('0199d7f2-8b1d-7c3a-a2e5-4f9c6d0b1e73'); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Webhooks.rotate_signing_secret("0199d7f2-8b1d-7c3a-a2e5-4f9c6d0b1e73") print(result) ``` ```php tab="PHP" webhooks->rotateSigningSecret('0199d7f2-8b1d-7c3a-a2e5-4f9c6d0b1e73'); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Webhooks.RotateSigningSecret(ctx, "0199d7f2-8b1d-7c3a-a2e5-4f9c6d0b1e73") if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Webhooks.rotate_signing_secret("0199d7f2-8b1d-7c3a-a2e5-4f9c6d0b1e73") puts result ``` ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/webhooks/0199d7f2-8b1d-7c3a-a2e5-4f9c6d0b1e73/signing-secret/rotate' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "object": "webhook", "id": "0199d7f2-8b1d-7c3a-a2e5-4f9c6d0b1e73", "signing_secret": "whsec_Q2w3E4r5T6y7U8i9O0pAsDfGhJkLzXcV" } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | No | Object type. | | `id` | string | No | Unique id (UUID). UUID. | | `signing_secret` | string | No | Secret (`whsec_…`) used to verify webhook signatures. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Create a template > Creates a template as a draft. Variables use triple braces, {{{ORDER_ID}}} or {{{NAME|fallback}}}, in subject, html and text; declare up to 50 typed variables (string or number) with optional… Source: https://docs.pidgely.com/api-reference/templates/create/ `POST https://api.pidgely.com/templates` Creates a template as a draft. Variables use triple braces, `{{{ORDER_ID}}}` or `{{{NAME|fallback}}}`, in `subject`, `html` and `text`; declare up to 50 typed variables (`string` or `number`) with optional fallbacks. `FIRST_NAME`, `LAST_NAME`, `EMAIL` and `UNSUBSCRIBE_URL` are reserved. [Publish](https://docs.pidgely.com/api-reference/templates/publish/) the draft before sending with it. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Body parameters JSON object. | Name | Type | Required | Description | |---|---|---|---| | `name` | string | No | Display name. | | `alias` | string | No | Unique, human-readable key you can use instead of the id: `^[A-Za-z0-9][A-Za-z0-9_.-]{0,99}$`, not a UUID. | | `from` | string | No | — | | `subject` | string | No | Subject line, up to 998 bytes. Required unless a template provides it. | | `reply_to` | string \| string[] | No | Reply-To address or list of addresses. | | `html` | string | No | HTML body. `html` or `text` is required (unless you send with a template). | | `text` | string | No | — | | `variables` | object[] | No | Typed variables (up to 50). `FIRST_NAME`, `LAST_NAME`, `EMAIL` and `UNSUBSCRIBE_URL` are reserved. | | `variables[].key` | string | Yes | Variable name, `^[A-Za-z0-9_]{1,50}$`. | | `variables[].type` | string | Yes | `string` or `number`. One of `string`, `number`. | | `variables[].fallback_value` | string \| number | No | Used when a send does not provide the variable. Must match the type. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.templates.create({ name: 'Order confirmation', alias: 'order-confirmation', from: 'Acme ', subject: 'Order {{{ORDER_ID}}} confirmed', html: '

Hi {{{CUSTOMER_NAME|there}}}, your order {{{ORDER_ID}}} totals {{{TOTAL}}}.

', variables: [ { key: 'ORDER_ID', type: 'string', }, { key: 'TOTAL', type: 'number', fallbackValue: 0, }, { key: 'CUSTOMER_NAME', type: 'string', fallbackValue: 'there', }, ], }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Templates.create({ "name": "Order confirmation", "alias": "order-confirmation", "from": "Acme ", "subject": "Order {{{ORDER_ID}}} confirmed", "html": "

Hi {{{CUSTOMER_NAME|there}}}, your order {{{ORDER_ID}}} totals {{{TOTAL}}}.

", "variables": [ { "key": "ORDER_ID", "type": "string", }, { "key": "TOTAL", "type": "number", "fallback_value": 0, }, { "key": "CUSTOMER_NAME", "type": "string", "fallback_value": "there", }, ], }) print(result) ``` ```php tab="PHP" templates->create([ 'name' => 'Order confirmation', 'alias' => 'order-confirmation', 'from' => 'Acme ', 'subject' => 'Order {{{ORDER_ID}}} confirmed', 'html' => '

Hi {{{CUSTOMER_NAME|there}}}, your order {{{ORDER_ID}}} totals {{{TOTAL}}}.

', 'variables' => [ [ 'key' => 'ORDER_ID', 'type' => 'string', ], [ 'key' => 'TOTAL', 'type' => 'number', 'fallback_value' => 0, ], [ 'key' => 'CUSTOMER_NAME', 'type' => 'string', 'fallback_value' => 'there', ], ], ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Templates.Create(ctx, &pidgely.CreateTemplateRequest{ Name: "Order confirmation", Alias: "order-confirmation", From: "Acme ", Subject: "Order {{{ORDER_ID}}} confirmed", Html: "

Hi {{{CUSTOMER_NAME|there}}}, your order {{{ORDER_ID}}} totals {{{TOTAL}}}.

", Variables: []pidgely.TemplateVariableInput{ { Key: "ORDER_ID", Type: "string", }, { Key: "TOTAL", Type: "number", FallbackValue: 0, }, { Key: "CUSTOMER_NAME", Type: "string", FallbackValue: "there", }, }, }) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Templates.create({ name: "Order confirmation", alias: "order-confirmation", from: "Acme ", subject: "Order {{{ORDER_ID}}} confirmed", html: "

Hi {{{CUSTOMER_NAME|there}}}, your order {{{ORDER_ID}}} totals {{{TOTAL}}}.

", variables: [ { key: "ORDER_ID", type: "string" }, { key: "TOTAL", type: "number", fallback_value: 0 }, { key: "CUSTOMER_NAME", type: "string", fallback_value: "there" } ] }) puts result ``` ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/templates' \ -H "Authorization: Bearer $PIDGELY_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "name": "Order confirmation", "alias": "order-confirmation", "from": "Acme ", "subject": "Order {{{ORDER_ID}}} confirmed", "html": "

Hi {{{CUSTOMER_NAME|there}}}, your order {{{ORDER_ID}}} totals {{{TOTAL}}}.

", "variables": [ { "key": "ORDER_ID", "type": "string" }, { "key": "TOTAL", "type": "number", "fallback_value": 0 }, { "key": "CUSTOMER_NAME", "type": "string", "fallback_value": "there" } ] }' ``` ## Response `200 OK` ```json { "object": "template", "id": "0199d7f7-7e2f-7b5c-8a4d-1c9e3f6a0b27" } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | Yes | Object type. | | `id` | string | Yes | Unique id (UUID). UUID. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | | 409 | `alias_taken` | Another template in the account already uses this alias. | --- # List templates > Lists templates, newest first (default page size 20). Source: https://docs.pidgely.com/api-reference/templates/list/ `GET https://api.pidgely.com/templates` Lists templates, newest first (default page size 20). **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `limit` | integer | No | Page size (1-100). Between 1 and 100. | | `after` | string | No | Id of the last item of the previous page (older items). UUID. | | `before` | string | No | Id of the first item of the current page (newer items). UUID. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.templates.list(); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Templates.list() print(result) ``` ```php tab="PHP" templates->list(); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Templates.List(ctx, nil) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Templates.list() puts result ``` ```bash tab="cURL" curl -X GET 'https://api.pidgely.com/templates' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "object": "list", "has_more": false, "data": [ { "object": "template", "id": "0199d7f7-7e2f-7b5c-8a4d-1c9e3f6a0b27", "current_version_id": "0199d7f7-8a11-7e3b-b5c4-0d2f6a9e1c83", "alias": "order-confirmation", "name": "Order confirmation", "created_at": "2026-10-12 16:44:41.000000+00", "updated_at": "2026-10-12 16:44:41.000000+00", "status": "published", "published_at": "2026-10-12 16:44:41.000000+00", "from": "Acme ", "subject": "Order {{{ORDER_ID}}} confirmed", "reply_to": [], "has_unpublished_versions": false } ] } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | Yes | Object type. One of `list`. | | `has_more` | boolean | Yes | Whether another page exists. | | `data` | object[] | Yes | Items of this page. | | `data[].object` | string | No | Object type. | | `data[].id` | string | No | Unique id (UUID). UUID. | | `data[].current_version_id` | string \| null | No | Id of the published version, or null. | | `data[].alias` | string \| null | No | Unique, human-readable key you can use instead of the id: `^[A-Za-z0-9][A-Za-z0-9_.-]{0,99}$`, not a UUID. | | `data[].name` | string | No | Display name. | | `data[].created_at` | string | No | Timestamp in UTC (`2026-10-12 16:44:41.000000+00`). | | `data[].updated_at` | string | No | Timestamp in UTC (`2026-10-12 16:44:41.000000+00`). | | `data[].status` | string | No | Current status. One of `draft`, `published`. | | `data[].published_at` | string \| null | No | Timestamp in UTC, or null. | | `data[].from` | string | No | — | | `data[].subject` | string | No | Subject line, up to 998 bytes. Required unless a template provides it. | | `data[].reply_to` | string[] | No | Reply-To address or list of addresses. | | `data[].html` | string | No | HTML body. `html` or `text` is required (unless you send with a template). | | `data[].text` | string | No | — | | `data[].variables` | object[] | No | Typed variables (up to 50). `FIRST_NAME`, `LAST_NAME`, `EMAIL` and `UNSUBSCRIBE_URL` are reserved. | | `data[].variables[].id` | string | No | Unique id (UUID). UUID. | | `data[].variables[].key` | string | No | Variable name, `^[A-Za-z0-9_]{1,50}$`. | | `data[].variables[].type` | string | No | `string` or `number`. One of `string`, `number`. | | `data[].variables[].fallback_value` | string \| number \| null | No | Used when a send does not provide the variable. Must match the type. | | `data[].variables[].created_at` | string | No | Timestamp in UTC (`2026-10-12 16:44:41.000000+00`). | | `data[].variables[].updated_at` | string | No | Timestamp in UTC (`2026-10-12 16:44:41.000000+00`). | | `data[].has_unpublished_versions` | boolean | No | Whether the draft differs from the published version. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Retrieve a template (draft) > Retrieves a template by id or alias. The content returned is the current draft; has_unpublished_versions tells you whether it differs from the published version. Source: https://docs.pidgely.com/api-reference/templates/retrieve/ `GET https://api.pidgely.com/templates/{key}` Retrieves a template by id or alias. The content returned is the current **draft**; `has_unpublished_versions` tells you whether it differs from the published version. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `key` | string | Yes | Template id or alias | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.templates.get('order-confirmation'); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Templates.get("order-confirmation") print(result) ``` ```php tab="PHP" templates->get('order-confirmation'); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Templates.Get(ctx, "order-confirmation") if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Templates.get("order-confirmation") puts result ``` ```bash tab="cURL" curl -X GET 'https://api.pidgely.com/templates/order-confirmation' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "object": "template", "id": "0199d7f7-7e2f-7b5c-8a4d-1c9e3f6a0b27", "current_version_id": "0199d7f7-8a11-7e3b-b5c4-0d2f6a9e1c83", "alias": "order-confirmation", "name": "Order confirmation", "created_at": "2026-10-12 16:44:41.000000+00", "updated_at": "2026-10-12 16:44:41.000000+00", "status": "published", "published_at": "2026-10-12 16:44:41.000000+00", "from": "Acme ", "subject": "Order {{{ORDER_ID}}} confirmed", "reply_to": [], "html": "

Hi {{{CUSTOMER_NAME|there}}}, your order {{{ORDER_ID}}} totals {{{TOTAL}}}.

", "text": "", "variables": [ { "id": "0199d7f7-7e30-7c1d-9f2a-6b4e8d0c3a51", "key": "ORDER_ID", "type": "string", "fallback_value": null, "created_at": "2026-10-12 16:44:41.000000+00", "updated_at": "2026-10-12 16:44:41.000000+00" }, { "id": "0199d7f7-7e30-7c1d-9f2a-6b4e8d0c3a52", "key": "TOTAL", "type": "number", "fallback_value": 0, "created_at": "2026-10-12 16:44:41.000000+00", "updated_at": "2026-10-12 16:44:41.000000+00" } ], "has_unpublished_versions": false } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | No | Object type. | | `id` | string | No | Unique id (UUID). UUID. | | `current_version_id` | string \| null | No | Id of the published version, or null. | | `alias` | string \| null | No | Unique, human-readable key you can use instead of the id: `^[A-Za-z0-9][A-Za-z0-9_.-]{0,99}$`, not a UUID. | | `name` | string | No | Display name. | | `created_at` | string | No | Timestamp in UTC (`2026-10-12 16:44:41.000000+00`). | | `updated_at` | string | No | Timestamp in UTC (`2026-10-12 16:44:41.000000+00`). | | `status` | string | No | Current status. One of `draft`, `published`. | | `published_at` | string \| null | No | Timestamp in UTC, or null. | | `from` | string | No | — | | `subject` | string | No | Subject line, up to 998 bytes. Required unless a template provides it. | | `reply_to` | string[] | No | Reply-To address or list of addresses. | | `html` | string | No | HTML body. `html` or `text` is required (unless you send with a template). | | `text` | string | No | — | | `variables` | object[] | No | Typed variables (up to 50). `FIRST_NAME`, `LAST_NAME`, `EMAIL` and `UNSUBSCRIBE_URL` are reserved. | | `variables[].id` | string | No | Unique id (UUID). UUID. | | `variables[].key` | string | No | Variable name, `^[A-Za-z0-9_]{1,50}$`. | | `variables[].type` | string | No | `string` or `number`. One of `string`, `number`. | | `variables[].fallback_value` | string \| number \| null | No | Used when a send does not provide the variable. Must match the type. | | `variables[].created_at` | string | No | Timestamp in UTC (`2026-10-12 16:44:41.000000+00`). | | `variables[].updated_at` | string | No | Timestamp in UTC (`2026-10-12 16:44:41.000000+00`). | | `has_unpublished_versions` | boolean | No | Whether the draft differs from the published version. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Update a template draft > Updates the draft of a template (by id or alias). Emails keep using the published version until you publish again. Source: https://docs.pidgely.com/api-reference/templates/update/ `PATCH https://api.pidgely.com/templates/{key}` Updates the draft of a template (by id or alias). Emails keep using the published version until you publish again. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `key` | string | Yes | Template id or alias | ## Body parameters JSON object. | Name | Type | Required | Description | |---|---|---|---| | `name` | string | No | Display name. | | `alias` | string | No | Unique, human-readable key you can use instead of the id: `^[A-Za-z0-9][A-Za-z0-9_.-]{0,99}$`, not a UUID. | | `from` | string | No | — | | `subject` | string | No | Subject line, up to 998 bytes. Required unless a template provides it. | | `reply_to` | string \| string[] | No | Reply-To address or list of addresses. | | `html` | string | No | HTML body. `html` or `text` is required (unless you send with a template). | | `text` | string | No | — | | `variables` | object[] | No | Typed variables (up to 50). `FIRST_NAME`, `LAST_NAME`, `EMAIL` and `UNSUBSCRIBE_URL` are reserved. | | `variables[].key` | string | Yes | Variable name, `^[A-Za-z0-9_]{1,50}$`. | | `variables[].type` | string | Yes | `string` or `number`. One of `string`, `number`. | | `variables[].fallback_value` | string \| number | No | Used when a send does not provide the variable. Must match the type. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.templates.update('order-confirmation', { subject: 'Your order {{{ORDER_ID}}} is confirmed', }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Templates.update("order-confirmation", { "subject": "Your order {{{ORDER_ID}}} is confirmed", }) print(result) ``` ```php tab="PHP" templates->update('order-confirmation', [ 'subject' => 'Your order {{{ORDER_ID}}} is confirmed', ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Templates.Update(ctx, "order-confirmation", &pidgely.UpdateTemplateRequest{ Subject: "Your order {{{ORDER_ID}}} is confirmed", }) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Templates.update("order-confirmation", { subject: "Your order {{{ORDER_ID}}} is confirmed" }) puts result ``` ```bash tab="cURL" curl -X PATCH 'https://api.pidgely.com/templates/order-confirmation' \ -H "Authorization: Bearer $PIDGELY_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "subject": "Your order {{{ORDER_ID}}} is confirmed" }' ``` ## Response `200 OK` ```json { "object": "template", "id": "0199d7f7-7e2f-7b5c-8a4d-1c9e3f6a0b27" } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | Yes | Object type. | | `id` | string | Yes | Unique id (UUID). UUID. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | | 409 | `alias_taken` | Another template in the account already uses this alias. | --- # Delete a template > Deletes a template by id or alias. Source: https://docs.pidgely.com/api-reference/templates/delete/ `DELETE https://api.pidgely.com/templates/{key}` Deletes a template by id or alias. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `key` | string | Yes | Template id or alias | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.templates.remove('order-confirmation'); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Templates.remove("order-confirmation") print(result) ``` ```php tab="PHP" templates->remove('order-confirmation'); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Templates.Remove(ctx, "order-confirmation") if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Templates.remove("order-confirmation") puts result ``` ```bash tab="cURL" curl -X DELETE 'https://api.pidgely.com/templates/order-confirmation' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "object": "template", "id": "0199d7f7-7e2f-7b5c-8a4d-1c9e3f6a0b27", "deleted": true } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | Yes | Object type. | | `id` | string | Yes | Unique id (UUID). UUID. | | `deleted` | boolean | Yes | — | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Preview a template > Renders the draft of a template (by id or alias) with the variables you pass, without sending anything. Useful to check a draft before publishing. Source: https://docs.pidgely.com/api-reference/templates/preview/ `POST https://api.pidgely.com/templates/{key}/preview` Renders the **draft** of a template (by id or alias) with the variables you pass, without sending anything. Useful to check a draft before [publishing](https://docs.pidgely.com/api-reference/templates/publish/). **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `key` | string | Yes | Template id or alias | ## Body parameters JSON object. | Name | Type | Required | Description | |---|---|---|---| | `variables` | object (map of string \| number) | No | Typed variables (up to 50). `FIRST_NAME`, `LAST_NAME`, `EMAIL` and `UNSUBSCRIBE_URL` are reserved. | ## Request This endpoint is not in the [SDKs](https://docs.pidgely.com/sdks/) yet; call it over HTTP. ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/templates/order-confirmation/preview' \ -H "Authorization: Bearer $PIDGELY_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "variables": { "ORDER_ID": "1234", "TOTAL": 59.9 } }' ``` ## Response `200 OK` ```json { "object": "template", "from": "Acme ", "subject": "Hello from Acme", "reply_to": [ "reply_to" ], "html": "

Hello!

", "text": "Hello!", "missing_variables": [ "missing_variable" ] } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | No | Object type. | | `from` | string | No | — | | `subject` | string | Yes | Subject line, up to 998 bytes. Required unless a template provides it. | | `reply_to` | string[] | No | Reply-To address or list of addresses. | | `html` | string | Yes | HTML body. `html` or `text` is required (unless you send with a template). | | `text` | string | Yes | — | | `missing_variables` | string[] | Yes | — | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Publish a template > Turns the current draft into an immutable published version. Sends that reference the template (template: { id, variables }) use the latest published version. Source: https://docs.pidgely.com/api-reference/templates/publish/ `POST https://api.pidgely.com/templates/{key}/publish` Turns the current draft into an immutable published version. Sends that reference the template (`template: { id, variables }`) use the latest published version. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `key` | string | Yes | Template id or alias | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.templates.publish('order-confirmation'); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Templates.publish("order-confirmation") print(result) ``` ```php tab="PHP" templates->publish('order-confirmation'); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Templates.Publish(ctx, "order-confirmation") if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Templates.publish("order-confirmation") puts result ``` ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/templates/order-confirmation/publish' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "object": "template", "id": "0199d7f7-7e2f-7b5c-8a4d-1c9e3f6a0b27" } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | Yes | Object type. | | `id` | string | Yes | Unique id (UUID). UUID. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Duplicate a template > Creates a draft copy named (copy). Source: https://docs.pidgely.com/api-reference/templates/duplicate/ `POST https://api.pidgely.com/templates/{key}/duplicate` Creates a draft copy named ` (copy)`. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `key` | string | Yes | Template id or alias | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.templates.duplicate('order-confirmation'); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Templates.duplicate("order-confirmation") print(result) ``` ```php tab="PHP" templates->duplicate('order-confirmation'); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Templates.Duplicate(ctx, "order-confirmation") if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Templates.duplicate("order-confirmation") puts result ``` ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/templates/order-confirmation/duplicate' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "object": "template", "id": "0199d7f7-7e2f-7b5c-8a4d-1c9e3f6a0b27" } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | Yes | Object type. | | `id` | string | Yes | Unique id (UUID). UUID. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Create a contact > Creates a contact, or updates it when a contact with the same email already exists (contacts are unique per email in your account). Properties are typed key–value pairs (string up to 2,000… Source: https://docs.pidgely.com/api-reference/contacts/create/ `POST https://api.pidgely.com/contacts` Creates a contact, or updates it when a contact with the same email already exists (contacts are unique per email in your account). Properties are typed key–value pairs (string up to 2,000 characters, number, boolean; `null` deletes a key), up to 100 per contact. Pass `segments` to add memberships. A contact whose address already has an `unsubscribe` suppression starts as unsubscribed. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Body parameters JSON object. | Name | Type | Required | Description | |---|---|---|---| | `email` | string | No | Email address. | | `first_name` | string | No | First name, up to 200 characters. Available as `{{{FIRST_NAME}}}` in broadcasts. | | `last_name` | string | No | Last name, up to 200 characters. Available as `{{{LAST_NAME}}}` in broadcasts. | | `unsubscribed` | boolean | No | Whether the contact opted out of broadcasts. Kept in sync with the suppression list. | | `properties` | object (map of string \| number \| boolean \| null) | No | Custom properties: up to 100 keys matching `^[A-Za-z_][A-Za-z0-9_]{0,49}$`, values string (up to 2,000 characters), number, boolean, or `null` to delete. | | `segments` | object[] | No | Segments to add the contact to. | | `segments[].id` | string | Yes | Segment id. UUID. | | `topics` | any[] | No | Not supported yet. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.contacts.create({ email: 'steve@example.com', firstName: 'Steve', lastName: 'Wozniak', properties: { plan: 'pro', seats: 5, }, segments: [ { id: '0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48', }, ], }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Contacts.create({ "email": "steve@example.com", "first_name": "Steve", "last_name": "Wozniak", "properties": { "plan": "pro", "seats": 5, }, "segments": [ { "id": "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48", }, ], }) print(result) ``` ```php tab="PHP" contacts->create([ 'email' => 'steve@example.com', 'first_name' => 'Steve', 'last_name' => 'Wozniak', 'properties' => [ 'plan' => 'pro', 'seats' => 5, ], 'segments' => [ [ 'id' => '0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48', ], ], ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Contacts.Create(ctx, &pidgely.CreateContactRequest{ Email: "steve@example.com", FirstName: "Steve", LastName: "Wozniak", Properties: map[string]any{ "plan": "pro", "seats": 5, }, Segments: []pidgely.Segment{ { Id: "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48", }, }, }) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Contacts.create({ email: "steve@example.com", first_name: "Steve", last_name: "Wozniak", properties: { plan: "pro", seats: 5 }, segments: [ { id: "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48" } ] }) puts result ``` ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/contacts' \ -H "Authorization: Bearer $PIDGELY_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "email": "steve@example.com", "first_name": "Steve", "last_name": "Wozniak", "properties": { "plan": "pro", "seats": 5 }, "segments": [ { "id": "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48" } ] }' ``` ## Response `200 OK` ```json { "object": "contact", "id": "0199d7f4-5d7b-7a2c-b4e1-9f3c2a6d8e05" } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | Yes | Object type. | | `id` | string | Yes | Unique id (UUID). UUID. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # List contacts > Lists contacts, newest first. Filter by segment_id (or the legacy audience_id). Source: https://docs.pidgely.com/api-reference/contacts/list/ `GET https://api.pidgely.com/contacts` Lists contacts, newest first. Filter by `segment_id` (or the legacy `audience_id`). **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `segment_id` | string | No | UUID. | | `audience_id` | string | No | Legacy alias of segment_id. UUID. | | `limit` | integer | No | Page size (1-100). Between 1 and 100. | | `after` | string | No | Id of the last item of the previous page (older items). UUID. | | `before` | string | No | Id of the first item of the current page (newer items). UUID. | | `q` | string | No | Prefix of the e-mail, first or last name (Pidgely extension). | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.contacts.list({ segmentId: '0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48', limit: 50, }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Contacts.list({ "segment_id": "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48", "limit": 50, }) print(result) ``` ```php tab="PHP" contacts->list([ 'segment_id' => '0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48', 'limit' => 50, ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Contacts.List(ctx, &pidgely.ListContactsParams{ SegmentId: "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48", ListParams: pidgely.ListParams{ Limit: 50, }, }) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Contacts.list({ segment_id: "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48", limit: 50 }) puts result ``` ```bash tab="cURL" curl -X GET 'https://api.pidgely.com/contacts?segment_id=0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48&limit=50' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "object": "list", "has_more": false, "data": [ { "object": "contact", "id": "0199d7f4-5d7b-7a2c-b4e1-9f3c2a6d8e05", "email": "steve@example.com", "first_name": "Steve", "last_name": "Wozniak", "created_at": "2026-10-12 16:44:41.000000+00", "unsubscribed": false, "properties": { "plan": { "value": "pro", "type": "string" }, "seats": { "value": 5, "type": "number" } } } ] } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | Yes | Object type. One of `list`. | | `has_more` | boolean | Yes | Whether another page exists. | | `data` | object[] | Yes | Items of this page. | | `data[].object` | string | No | Object type. | | `data[].id` | string | No | Unique id (UUID). UUID. | | `data[].email` | string | No | Email address. | | `data[].first_name` | string | No | First name, up to 200 characters. Available as `{{{FIRST_NAME}}}` in broadcasts. | | `data[].last_name` | string | No | Last name, up to 200 characters. Available as `{{{LAST_NAME}}}` in broadcasts. | | `data[].created_at` | string | No | Timestamp in UTC (`2026-10-12 16:44:41.000000+00`). | | `data[].unsubscribed` | boolean | No | Whether the contact opted out of broadcasts. Kept in sync with the suppression list. | | `data[].properties` | object (map of object) | No | Custom properties: up to 100 keys matching `^[A-Za-z_][A-Za-z0-9_]{0,49}$`, values string (up to 2,000 characters), number, boolean, or `null` to delete. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Retrieve a contact > Retrieves a contact by id or email. Properties come back as { value, type }. Source: https://docs.pidgely.com/api-reference/contacts/retrieve/ `GET https://api.pidgely.com/contacts/{key}` Retrieves a contact by id or email. Properties come back as `{ value, type }`. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `key` | string | Yes | Contact id or e-mail address | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.contacts.get({ email: 'steve@example.com', }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Contacts.get({ "email": "steve@example.com", }) print(result) ``` ```php tab="PHP" contacts->get([ 'email' => 'steve@example.com', ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Contacts.Get(ctx, "steve@example.com") if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Contacts.get({ email: "steve@example.com" }) puts result ``` ```bash tab="cURL" curl -X GET 'https://api.pidgely.com/contacts/steve@example.com' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "object": "contact", "id": "0199d7f4-5d7b-7a2c-b4e1-9f3c2a6d8e05", "email": "steve@example.com", "first_name": "Steve", "last_name": "Wozniak", "created_at": "2026-10-12 16:44:41.000000+00", "unsubscribed": false, "properties": { "plan": { "value": "pro", "type": "string" }, "seats": { "value": 5, "type": "number" } } } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | No | Object type. | | `id` | string | No | Unique id (UUID). UUID. | | `email` | string | No | Email address. | | `first_name` | string | No | First name, up to 200 characters. Available as `{{{FIRST_NAME}}}` in broadcasts. | | `last_name` | string | No | Last name, up to 200 characters. Available as `{{{LAST_NAME}}}` in broadcasts. | | `created_at` | string | No | Timestamp in UTC (`2026-10-12 16:44:41.000000+00`). | | `unsubscribed` | boolean | No | Whether the contact opted out of broadcasts. Kept in sync with the suppression list. | | `properties` | object (map of object) | No | Custom properties: up to 100 keys matching `^[A-Za-z_][A-Za-z0-9_]{0,49}$`, values string (up to 2,000 characters), number, boolean, or `null` to delete. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Update a contact (unsubscribed syncs the suppression list) > Updates a contact by id or email. unsubscribed is kept in sync with the suppression list in both directions: true adds an unsubscribe suppression, false removes it (bounce and complaint… Source: https://docs.pidgely.com/api-reference/contacts/update/ `PATCH https://api.pidgely.com/contacts/{key}` Updates a contact by id or email. `unsubscribed` is kept in sync with the suppression list in both directions: `true` adds an `unsubscribe` suppression, `false` removes it (bounce and complaint suppressions are never removed this way). **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `key` | string | Yes | Contact id or e-mail address | ## Body parameters JSON object. | Name | Type | Required | Description | |---|---|---|---| | `email` | string | No | Email address. | | `first_name` | string | No | First name, up to 200 characters. Available as `{{{FIRST_NAME}}}` in broadcasts. | | `last_name` | string | No | Last name, up to 200 characters. Available as `{{{LAST_NAME}}}` in broadcasts. | | `unsubscribed` | boolean | No | Whether the contact opted out of broadcasts. Kept in sync with the suppression list. | | `properties` | object (map of string \| number \| boolean \| null) | No | Custom properties: up to 100 keys matching `^[A-Za-z_][A-Za-z0-9_]{0,49}$`, values string (up to 2,000 characters), number, boolean, or `null` to delete. | | `segments` | object[] | No | Segments to add the contact to. | | `segments[].id` | string | Yes | Segment id. UUID. | | `topics` | any[] | No | Not supported yet. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.contacts.update({ email: 'steve@example.com', firstName: 'Stephen', unsubscribed: false, properties: { seats: 10, }, }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Contacts.update({ "email": "steve@example.com", "first_name": "Stephen", "unsubscribed": False, "properties": { "seats": 10, }, }) print(result) ``` ```php tab="PHP" contacts->update([ 'email' => 'steve@example.com', 'first_name' => 'Stephen', 'unsubscribed' => false, 'properties' => [ 'seats' => 10, ], ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Contacts.Update(ctx, "steve@example.com", &pidgely.UpdateContactRequest{ FirstName: "Stephen", Unsubscribed: pidgely.Bool(false), Properties: map[string]any{ "seats": 10, }, }) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Contacts.update({ email: "steve@example.com", first_name: "Stephen", unsubscribed: false, properties: { seats: 10 } }) puts result ``` ```bash tab="cURL" curl -X PATCH 'https://api.pidgely.com/contacts/steve@example.com' \ -H "Authorization: Bearer $PIDGELY_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "first_name": "Stephen", "unsubscribed": false, "properties": { "seats": 10 } }' ``` ## Response `200 OK` ```json { "object": "contact", "id": "0199d7f4-5d7b-7a2c-b4e1-9f3c2a6d8e05" } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | Yes | Object type. | | `id` | string | Yes | Unique id (UUID). UUID. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Delete a contact > Deletes a contact by id or email. Its suppression entry, if any, is kept. Source: https://docs.pidgely.com/api-reference/contacts/delete/ `DELETE https://api.pidgely.com/contacts/{key}` Deletes a contact by id or email. Its suppression entry, if any, is kept. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `key` | string | Yes | Contact id or e-mail address | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.contacts.remove({ email: 'steve@example.com', }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Contacts.remove({ "email": "steve@example.com", }) print(result) ``` ```php tab="PHP" contacts->remove([ 'email' => 'steve@example.com', ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Contacts.Remove(ctx, "steve@example.com") if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Contacts.remove({ email: "steve@example.com" }) puts result ``` ```bash tab="cURL" curl -X DELETE 'https://api.pidgely.com/contacts/steve@example.com' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "object": "contact", "contact": "0199d7f4-5d7b-7a2c-b4e1-9f3c2a6d8e05", "id": "0199d7f4-5d7b-7a2c-b4e1-9f3c2a6d8e05", "deleted": true } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | No | Object type. | | `contact` | string | No | UUID. | | `id` | string | No | Unique id (UUID). UUID. | | `deleted` | boolean | No | — | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Upsert contacts in batch > Creates or updates up to 1,000 contacts in one request (each row is an upsert by email, like Create a contact). Rows are saved independently: invalid rows are reported in errors with their index… Source: https://docs.pidgely.com/api-reference/contacts/post-batch/ `POST https://api.pidgely.com/contacts/batch` Creates or updates up to 1,000 contacts in one request (each row is an upsert by email, like [Create a contact](https://docs.pidgely.com/api-reference/contacts/create/)). Rows are saved independently: invalid rows are reported in `errors` with their index and the others are kept. Pass `segment_id` to add every row to a segment. Each row is saved independently; invalid rows are reported in errors with their index. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Body parameters JSON object. | Name | Type | Required | Description | |---|---|---|---| | `contacts` | object[] | Yes | — | | `contacts[].email` | string | No | Email address. | | `contacts[].first_name` | string | No | First name, up to 200 characters. Available as `{{{FIRST_NAME}}}` in broadcasts. | | `contacts[].last_name` | string | No | Last name, up to 200 characters. Available as `{{{LAST_NAME}}}` in broadcasts. | | `contacts[].unsubscribed` | boolean | No | Whether the contact opted out of broadcasts. Kept in sync with the suppression list. | | `contacts[].properties` | object (map of string \| number \| boolean \| null) | No | Custom properties: up to 100 keys matching `^[A-Za-z_][A-Za-z0-9_]{0,49}$`, values string (up to 2,000 characters), number, boolean, or `null` to delete. | | `contacts[].segments` | object[] | No | Segments to add the contact to. | | `contacts[].segments[].id` | string | Yes | Unique id (UUID). UUID. | | `contacts[].topics` | any[] | No | Not supported yet. | | `segment_id` | string | No | Added to every row. UUID. | ## Request This endpoint is not in the [SDKs](https://docs.pidgely.com/sdks/) yet; call it over HTTP. ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/contacts/batch' \ -H "Authorization: Bearer $PIDGELY_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "segment_id": "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48", "contacts": [ { "email": "steve@example.com", "first_name": "Steve" }, { "email": "ada@example.com", "first_name": "Ada", "properties": { "plan": "pro" } } ] }' ``` ## Response `200 OK` ```json { "object": "contact", "data": [ { "index": 1, "id": "0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21" } ], "errors": [ { "index": 1, "message": "The `to` field is required." } ] } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | Yes | Object type. One of `list`. | | `data` | object[] | Yes | Items of this page. | | `data[].index` | integer | Yes | — | | `data[].id` | string | Yes | Unique id (UUID). UUID. | | `errors` | object[] | Yes | — | | `errors[].index` | integer | Yes | — | | `errors[].message` | string | Yes | — | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # List a contact's segments > Lists the segments a contact belongs to. Source: https://docs.pidgely.com/api-reference/contacts/list-segments/ `GET https://api.pidgely.com/contacts/{key}/segments` Lists the segments a contact belongs to. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `key` | string | Yes | Contact id or e-mail address | ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `limit` | integer | No | Page size (1-100). Between 1 and 100. | | `after` | string | No | Id of the last item of the previous page (older items). UUID. | | `before` | string | No | Id of the first item of the current page (newer items). UUID. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.contacts.segments.list({ email: 'steve@example.com', }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Contacts.Segments.list({ "email": "steve@example.com", }) print(result) ``` ```php tab="PHP" contacts->segments->list([ 'email' => 'steve@example.com', ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Contacts.Segments.List(ctx, "steve@example.com", nil) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Contacts::Segments.list({ email: "steve@example.com" }) puts result ``` ```bash tab="cURL" curl -X GET 'https://api.pidgely.com/contacts/steve@example.com/segments' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "object": "list", "has_more": false, "data": [ { "object": "segment", "id": "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48", "name": "Newsletter", "created_at": "2026-10-12 16:44:41.000000+00" } ] } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | Yes | Object type. One of `list`. | | `has_more` | boolean | Yes | Whether another page exists. | | `data` | object[] | Yes | Items of this page. | | `data[].object` | string | No | Object type. | | `data[].id` | string | No | Unique id (UUID). UUID. | | `data[].name` | string | No | Display name. | | `data[].created_at` | string | No | Timestamp in UTC (`2026-10-12 16:44:41.000000+00`). | | `data[].contacts_count` | integer | No | Number of contacts in the segment. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Add a contact to a segment > Adds a contact to a segment. Source: https://docs.pidgely.com/api-reference/contacts/add-segment/ `POST https://api.pidgely.com/contacts/{key}/segments/{segment_id}` Adds a contact to a segment. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `key` | string | Yes | Contact id or e-mail address | | `segment_id` | string | Yes | Segment id UUID. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.contacts.segments.add({ email: 'steve@example.com', segmentId: '0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48', }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Contacts.Segments.add({ "email": "steve@example.com", "segment_id": "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48", }) print(result) ``` ```php tab="PHP" contacts->segments->add([ 'email' => 'steve@example.com', 'segment_id' => '0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48', ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Contacts.Segments.Add(ctx, "steve@example.com", "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48") if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Contacts::Segments.add({ email: "steve@example.com", segment_id: "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48" }) puts result ``` ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/contacts/steve@example.com/segments/0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "id": "0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21" } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | Unique id (UUID). UUID. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Remove a contact from a segment > Removes a contact from a segment. The contact itself is kept. Source: https://docs.pidgely.com/api-reference/contacts/remove-segment/ `DELETE https://api.pidgely.com/contacts/{key}/segments/{segment_id}` Removes a contact from a segment. The contact itself is kept. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `key` | string | Yes | Contact id or e-mail address | | `segment_id` | string | Yes | Segment id UUID. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.contacts.segments.remove({ email: 'steve@example.com', segmentId: '0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48', }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Contacts.Segments.remove({ "email": "steve@example.com", "segment_id": "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48", }) print(result) ``` ```php tab="PHP" contacts->segments->remove([ 'email' => 'steve@example.com', 'segment_id' => '0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48', ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Contacts.Segments.Remove(ctx, "steve@example.com", "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48") if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Contacts::Segments.remove({ email: "steve@example.com", segment_id: "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48" }) puts result ``` ```bash tab="cURL" curl -X DELETE 'https://api.pidgely.com/contacts/steve@example.com/segments/0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "object": "contact", "id": "0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21", "segment_id": "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48", "audienceId": "0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21", "deleted": true } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | No | Object type. | | `id` | string | No | Unique id (UUID). UUID. | | `segment_id` | string | No | Segment the broadcast is sent to. UUID. | | `audienceId` | string | No | UUID. | | `deleted` | boolean | No | — | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Create a segment > Creates a segment: a named group of contacts that broadcasts are sent to. Source: https://docs.pidgely.com/api-reference/segments/create/ `POST https://api.pidgely.com/segments` Creates a segment: a named group of contacts that [broadcasts](https://docs.pidgely.com/guides/broadcasts/) are sent to. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Body parameters JSON object. | Name | Type | Required | Description | |---|---|---|---| | `name` | string | Yes | Display name. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.segments.create({ name: 'Newsletter', }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Segments.create({ "name": "Newsletter", }) print(result) ``` ```php tab="PHP" segments->create([ 'name' => 'Newsletter', ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Segments.Create(ctx, &pidgely.CreateSegmentRequest{ Name: "Newsletter", }) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Segments.create({ name: "Newsletter" }) puts result ``` ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/segments' \ -H "Authorization: Bearer $PIDGELY_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "name": "Newsletter" }' ``` ## Response `200 OK` ```json { "object": "segment", "id": "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48", "name": "Newsletter" } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | No | Object type. | | `id` | string | No | Unique id (UUID). UUID. | | `name` | string | No | Display name. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # List segments > Lists segments, newest first. Source: https://docs.pidgely.com/api-reference/segments/list/ `GET https://api.pidgely.com/segments` Lists segments, newest first. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `limit` | integer | No | Page size (1-100). Between 1 and 100. | | `after` | string | No | Id of the last item of the previous page (older items). UUID. | | `before` | string | No | Id of the first item of the current page (newer items). UUID. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.segments.list(); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Segments.list() print(result) ``` ```php tab="PHP" segments->list(); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Segments.List(ctx, nil) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Segments.list() puts result ``` ```bash tab="cURL" curl -X GET 'https://api.pidgely.com/segments' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "object": "list", "has_more": false, "data": [ { "object": "segment", "id": "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48", "name": "Newsletter", "created_at": "2026-10-12 16:44:41.000000+00" } ] } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | Yes | Object type. One of `list`. | | `has_more` | boolean | Yes | Whether another page exists. | | `data` | object[] | Yes | Items of this page. | | `data[].object` | string | No | Object type. | | `data[].id` | string | No | Unique id (UUID). UUID. | | `data[].name` | string | No | Display name. | | `data[].created_at` | string | No | Timestamp in UTC (`2026-10-12 16:44:41.000000+00`). | | `data[].contacts_count` | integer | No | Number of contacts in the segment. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Retrieve a segment > Retrieves a segment. Source: https://docs.pidgely.com/api-reference/segments/retrieve/ `GET https://api.pidgely.com/segments/{id}` Retrieves a segment. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | Resource id UUID. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.segments.get('0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48'); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Segments.get("0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48") print(result) ``` ```php tab="PHP" segments->get('0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48'); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Segments.Get(ctx, "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48") if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Segments.get("0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48") puts result ``` ```bash tab="cURL" curl -X GET 'https://api.pidgely.com/segments/0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "object": "segment", "id": "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48", "name": "Newsletter", "created_at": "2026-10-12 16:44:41.000000+00" } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | No | Object type. | | `id` | string | No | Unique id (UUID). UUID. | | `name` | string | No | Display name. | | `created_at` | string | No | Timestamp in UTC (`2026-10-12 16:44:41.000000+00`). | | `contacts_count` | integer | No | Number of contacts in the segment. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Rename a segment > Renames a segment. Source: https://docs.pidgely.com/api-reference/segments/update/ `PATCH https://api.pidgely.com/segments/{id}` Renames a segment. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | Resource id UUID. | ## Body parameters JSON object. | Name | Type | Required | Description | |---|---|---|---| | `name` | string | Yes | Display name. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.segments.update('0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48', { name: 'Product updates', }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Segments.update("0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48", { "name": "Product updates", }) print(result) ``` ```php tab="PHP" segments->update('0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48', [ 'name' => 'Product updates', ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Segments.Update(ctx, "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48", &pidgely.UpdateSegmentRequest{ Name: "Product updates", }) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Segments.update("0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48", { name: "Product updates" }) puts result ``` ```bash tab="cURL" curl -X PATCH 'https://api.pidgely.com/segments/0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48' \ -H "Authorization: Bearer $PIDGELY_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "name": "Product updates" }' ``` ## Response `200 OK` ```json { "object": "segment", "id": "0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21" } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | Yes | Object type. | | `id` | string | Yes | Unique id (UUID). UUID. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Delete a segment > Deletes a segment. Its contacts are kept; only the memberships are removed. Source: https://docs.pidgely.com/api-reference/segments/delete/ `DELETE https://api.pidgely.com/segments/{id}` Deletes a segment. Its contacts are kept; only the memberships are removed. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | Resource id UUID. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.segments.remove('0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48'); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Segments.remove("0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48") print(result) ``` ```php tab="PHP" segments->remove('0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48'); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Segments.Remove(ctx, "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48") if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Segments.remove("0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48") puts result ``` ```bash tab="cURL" curl -X DELETE 'https://api.pidgely.com/segments/0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "object": "segment", "id": "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48", "deleted": true } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | Yes | Object type. | | `id` | string | Yes | Unique id (UUID). UUID. | | `deleted` | boolean | Yes | — | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # List a segment's contacts > Lists the contacts in a segment. Source: https://docs.pidgely.com/api-reference/segments/list-contacts/ `GET https://api.pidgely.com/segments/{id}/contacts` Lists the contacts in a segment. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | Resource id UUID. | ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `limit` | integer | No | Page size (1-100). Between 1 and 100. | | `after` | string | No | Id of the last item of the previous page (older items). UUID. | | `before` | string | No | Id of the first item of the current page (newer items). UUID. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.segments.listContacts('0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48'); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Segments.list_contacts("0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48") print(result) ``` ```php tab="PHP" segments->listContacts('0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48'); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Segments.ListContacts(ctx, "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48", nil) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Segments.list_contacts("0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48") puts result ``` ```bash tab="cURL" curl -X GET 'https://api.pidgely.com/segments/0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48/contacts' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "object": "list", "has_more": false, "data": [ { "object": "contact", "id": "0199d7f4-5d7b-7a2c-b4e1-9f3c2a6d8e05", "email": "steve@example.com", "first_name": "Steve", "last_name": "Wozniak", "created_at": "2026-10-12 16:44:41.000000+00", "unsubscribed": false, "properties": { "plan": { "value": "pro", "type": "string" }, "seats": { "value": 5, "type": "number" } } } ] } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | Yes | Object type. One of `list`. | | `has_more` | boolean | Yes | Whether another page exists. | | `data` | object[] | Yes | Items of this page. | | `data[].object` | string | No | Object type. | | `data[].id` | string | No | Unique id (UUID). UUID. | | `data[].email` | string | No | Email address. | | `data[].first_name` | string | No | First name, up to 200 characters. Available as `{{{FIRST_NAME}}}` in broadcasts. | | `data[].last_name` | string | No | Last name, up to 200 characters. Available as `{{{LAST_NAME}}}` in broadcasts. | | `data[].created_at` | string | No | Timestamp in UTC (`2026-10-12 16:44:41.000000+00`). | | `data[].unsubscribed` | boolean | No | Whether the contact opted out of broadcasts. Kept in sync with the suppression list. | | `data[].properties` | object (map of object) | No | Custom properties: up to 100 keys matching `^[A-Za-z_][A-Za-z0-9_]{0,49}$`, values string (up to 2,000 characters), number, boolean, or `null` to delete. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Create a broadcast > Creates a broadcast to a segment. segment_id, from and subject are required (audience_id is accepted as a legacy alias). It is saved as a draft unless you pass send: true or scheduled_at, in which… Source: https://docs.pidgely.com/api-reference/broadcasts/create/ `POST https://api.pidgely.com/broadcasts` Creates a broadcast to a segment. `segment_id`, `from` and `subject` are required (`audience_id` is accepted as a legacy alias). It is saved as a draft unless you pass `send: true` or `scheduled_at`, in which case it is sent right away or at that time. The body supports [personalization](https://docs.pidgely.com/guides/broadcasts/#personalization): `{{{FIRST_NAME|there}}}`, `{{{contact.}}}` and the unsubscribe link `{{{PIDGELY_UNSUBSCRIBE_URL}}}`. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Body parameters JSON object. | Name | Type | Required | Description | |---|---|---|---| | `segment_id` | string | No | Segment the broadcast is sent to. UUID. | | `audience_id` | string | No | Legacy alias of segment_id. UUID. | | `from` | string | No | — | | `subject` | string | No | Subject line, up to 998 bytes. Required unless a template provides it. | | `reply_to` | string \| string[] | No | Reply-To address or list of addresses. | | `html` | string | No | Supports `{{{FIRST_NAME\|there}}}`, `{{{contact.}}}`, `{{{PIDGELY_UNSUBSCRIBE_URL}}}`. | | `text` | string | No | — | | `name` | string | No | Display name. | | `preview_text` | string | No | Preview text shown by mail clients next to the subject (inserted as hidden text at the top of the HTML). | | `send` | boolean | No | Send immediately after creating (or at `scheduled_at`). | | `scheduled_at` | string | No | ISO 8601 or natural language (`in 1 hour`, `tomorrow at 9am`, UTC), within 30 days. | | `topic_id` | string | No | Not supported yet. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.broadcasts.create({ segmentId: '0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48', name: 'October product update', from: 'Acme ', subject: 'What shipped in October', previewText: 'Five new features and a faster dashboard', html: '

Hi {{{FIRST_NAME|there}}},

…

Unsubscribe

', }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Broadcasts.create({ "segment_id": "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48", "name": "October product update", "from": "Acme ", "subject": "What shipped in October", "preview_text": "Five new features and a faster dashboard", "html": "

Hi {{{FIRST_NAME|there}}},

…

Unsubscribe

", }) print(result) ``` ```php tab="PHP" broadcasts->create([ 'segment_id' => '0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48', 'name' => 'October product update', 'from' => 'Acme ', 'subject' => 'What shipped in October', 'preview_text' => 'Five new features and a faster dashboard', 'html' => '

Hi {{{FIRST_NAME|there}}},

…

Unsubscribe

', ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Broadcasts.Create(ctx, &pidgely.CreateBroadcastRequest{ SegmentId: "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48", Name: "October product update", From: "Acme ", Subject: "What shipped in October", PreviewText: "Five new features and a faster dashboard", Html: "

Hi {{{FIRST_NAME|there}}},

…

Unsubscribe

", }) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Broadcasts.create({ segment_id: "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48", name: "October product update", from: "Acme ", subject: "What shipped in October", preview_text: "Five new features and a faster dashboard", html: "

Hi {{{FIRST_NAME|there}}},

…

Unsubscribe

" }) puts result ``` ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/broadcasts' \ -H "Authorization: Bearer $PIDGELY_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "segment_id": "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48", "name": "October product update", "from": "Acme ", "subject": "What shipped in October", "preview_text": "Five new features and a faster dashboard", "html": "

Hi {{{FIRST_NAME|there}}},

…

Unsubscribe

" }' ``` ## Response `200 OK` ```json { "object": "broadcast", "id": "0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19" } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | Yes | Object type. | | `id` | string | Yes | Unique id (UUID). UUID. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # List broadcasts > Lists broadcasts, newest first. Source: https://docs.pidgely.com/api-reference/broadcasts/list/ `GET https://api.pidgely.com/broadcasts` Lists broadcasts, newest first. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `limit` | integer | No | Page size (1-100). Between 1 and 100. | | `after` | string | No | Id of the last item of the previous page (older items). UUID. | | `before` | string | No | Id of the first item of the current page (newer items). UUID. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.broadcasts.list(); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Broadcasts.list() print(result) ``` ```php tab="PHP" broadcasts->list(); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Broadcasts.List(ctx, nil) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Broadcasts.list() puts result ``` ```bash tab="cURL" curl -X GET 'https://api.pidgely.com/broadcasts' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "object": "list", "has_more": false, "data": [ { "object": "broadcast", "id": "0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19", "name": "October product update", "segment_id": "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48", "audience_id": "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48", "from": "Acme ", "subject": "What shipped in October", "reply_to": [], "preview_text": "Five new features and a faster dashboard", "html": "

Hi {{{FIRST_NAME|there}}},

…

Unsubscribe

", "text": "", "status": "draft", "created_at": "2026-10-12 16:44:41.000000+00", "scheduled_at": null, "sent_at": null, "topic_id": null, "recipients": 0, "stats": {} } ] } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | Yes | Object type. One of `list`. | | `has_more` | boolean | Yes | Whether another page exists. | | `data` | object[] | Yes | Items of this page. | | `data[].object` | string | No | Object type. | | `data[].id` | string | No | Unique id (UUID). UUID. | | `data[].name` | string | No | Display name. | | `data[].segment_id` | string \| null | No | Segment the broadcast is sent to. | | `data[].audience_id` | string \| null | No | — | | `data[].from` | string | No | — | | `data[].subject` | string | No | Subject line, up to 998 bytes. Required unless a template provides it. | | `data[].reply_to` | string[] | No | Reply-To address or list of addresses. | | `data[].preview_text` | string | No | Preview text shown by mail clients next to the subject (inserted as hidden text at the top of the HTML). | | `data[].html` | string | No | HTML body. `html` or `text` is required (unless you send with a template). | | `data[].text` | string | No | — | | `data[].status` | string | No | Current status. One of `draft`, `scheduled`, `queued`, `sending`, `sent`, `canceled`, `failed`. | | `data[].created_at` | string | No | Timestamp in UTC (`2026-10-12 16:44:41.000000+00`). | | `data[].scheduled_at` | string \| null | No | Timestamp in UTC, or null. | | `data[].sent_at` | string \| null | No | Timestamp in UTC, or null. | | `data[].topic_id` | null | No | — | | `data[].recipients` | integer | No | Number of contacts the broadcast was sent to. | | `data[].stats` | object (map of integer) | No | Deliveries by status (Pidgely extension). | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Retrieve a broadcast > Retrieves a broadcast with its status (draft, scheduled, queued, sending, sent, canceled or failed), recipients and per-status delivery stats. Source: https://docs.pidgely.com/api-reference/broadcasts/retrieve/ `GET https://api.pidgely.com/broadcasts/{id}` Retrieves a broadcast with its `status` (`draft`, `scheduled`, `queued`, `sending`, `sent`, `canceled` or `failed`), `recipients` and per-status delivery `stats`. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | Resource id UUID. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.broadcasts.get('0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19'); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Broadcasts.get("0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19") print(result) ``` ```php tab="PHP" broadcasts->get('0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19'); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Broadcasts.Get(ctx, "0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19") if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Broadcasts.get("0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19") puts result ``` ```bash tab="cURL" curl -X GET 'https://api.pidgely.com/broadcasts/0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "object": "broadcast", "id": "0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19", "name": "October product update", "segment_id": "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48", "audience_id": "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48", "from": "Acme ", "subject": "What shipped in October", "reply_to": [], "preview_text": "Five new features and a faster dashboard", "html": "

Hi {{{FIRST_NAME|there}}},

…

Unsubscribe

", "text": "", "status": "sent", "created_at": "2026-10-12 16:44:41.000000+00", "scheduled_at": null, "sent_at": "2026-10-12 16:44:41.000000+00", "topic_id": null, "recipients": 1200, "stats": { "delivered": 1187, "bounced": 4, "suppressed": 9 } } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | No | Object type. | | `id` | string | No | Unique id (UUID). UUID. | | `name` | string | No | Display name. | | `segment_id` | string \| null | No | Segment the broadcast is sent to. | | `audience_id` | string \| null | No | — | | `from` | string | No | — | | `subject` | string | No | Subject line, up to 998 bytes. Required unless a template provides it. | | `reply_to` | string[] | No | Reply-To address or list of addresses. | | `preview_text` | string | No | Preview text shown by mail clients next to the subject (inserted as hidden text at the top of the HTML). | | `html` | string | No | HTML body. `html` or `text` is required (unless you send with a template). | | `text` | string | No | — | | `status` | string | No | Current status. One of `draft`, `scheduled`, `queued`, `sending`, `sent`, `canceled`, `failed`. | | `created_at` | string | No | Timestamp in UTC (`2026-10-12 16:44:41.000000+00`). | | `scheduled_at` | string \| null | No | Timestamp in UTC, or null. | | `sent_at` | string \| null | No | Timestamp in UTC, or null. | | `topic_id` | null | No | — | | `recipients` | integer | No | Number of contacts the broadcast was sent to. | | `stats` | object (map of integer) | No | Deliveries by status (Pidgely extension). | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Update a draft broadcast > Updates a draft broadcast. Broadcasts that are scheduled or already sending cannot be edited. Source: https://docs.pidgely.com/api-reference/broadcasts/update/ `PATCH https://api.pidgely.com/broadcasts/{id}` Updates a draft broadcast. Broadcasts that are scheduled or already sending cannot be edited. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | Resource id UUID. | ## Body parameters JSON object. | Name | Type | Required | Description | |---|---|---|---| | `segment_id` | string | No | Segment the broadcast is sent to. UUID. | | `audience_id` | string | No | Legacy alias of segment_id. UUID. | | `from` | string | No | — | | `subject` | string | No | Subject line, up to 998 bytes. Required unless a template provides it. | | `reply_to` | string \| string[] | No | Reply-To address or list of addresses. | | `html` | string | No | Supports `{{{FIRST_NAME\|there}}}`, `{{{contact.}}}`, `{{{PIDGELY_UNSUBSCRIBE_URL}}}`. | | `text` | string | No | — | | `name` | string | No | Display name. | | `preview_text` | string | No | Preview text shown by mail clients next to the subject (inserted as hidden text at the top of the HTML). | | `send` | boolean | No | Send immediately after creating (or at `scheduled_at`). | | `scheduled_at` | string | No | ISO 8601 or natural language (`in 1 hour`, `tomorrow at 9am`, UTC), within 30 days. | | `topic_id` | string | No | Not supported yet. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.broadcasts.update('0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19', { subject: 'What shipped in October (and what is next)', }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Broadcasts.update("0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19", { "subject": "What shipped in October (and what is next)", }) print(result) ``` ```php tab="PHP" broadcasts->update('0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19', [ 'subject' => 'What shipped in October (and what is next)', ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Broadcasts.Update(ctx, "0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19", &pidgely.UpdateBroadcastRequest{ Subject: "What shipped in October (and what is next)", }) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Broadcasts.update("0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19", { subject: "What shipped in October (and what is next)" }) puts result ``` ```bash tab="cURL" curl -X PATCH 'https://api.pidgely.com/broadcasts/0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19' \ -H "Authorization: Bearer $PIDGELY_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "subject": "What shipped in October (and what is next)" }' ``` ## Response `200 OK` ```json { "object": "broadcast", "id": "0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19" } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | Yes | Object type. | | `id` | string | Yes | Unique id (UUID). UUID. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | | 409 | `broadcast_not_draft` | The broadcast is not a draft any more. | --- # Delete a broadcast > Deletes a broadcast. Broadcasts that are scheduled, queued or sending must be canceled first. Source: https://docs.pidgely.com/api-reference/broadcasts/delete/ `DELETE https://api.pidgely.com/broadcasts/{id}` Deletes a broadcast. Broadcasts that are scheduled, queued or sending must be canceled first. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | Resource id UUID. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.broadcasts.remove('0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19'); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Broadcasts.remove("0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19") print(result) ``` ```php tab="PHP" broadcasts->remove('0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19'); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Broadcasts.Remove(ctx, "0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19") if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Broadcasts.remove("0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19") puts result ``` ```bash tab="cURL" curl -X DELETE 'https://api.pidgely.com/broadcasts/0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "object": "broadcast", "id": "0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19", "deleted": true } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | Yes | Object type. | | `id` | string | Yes | Unique id (UUID). UUID. | | `deleted` | boolean | Yes | — | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | | 409 | `broadcast_in_progress` | The broadcast is scheduled, queued or sending. | --- # Send a broadcast > Sends a draft broadcast now, or at scheduled_at (ISO 8601 or natural language, within 30 days). The broadcast needs a segment, a subject, html or text, and a from on a verified domain. Only… Source: https://docs.pidgely.com/api-reference/broadcasts/send/ `POST https://api.pidgely.com/broadcasts/{id}/send` Sends a draft broadcast now, or at `scheduled_at` (ISO 8601 or natural language, within 30 days). The broadcast needs a segment, a subject, `html` or `text`, and a `from` on a verified domain. Only subscribed contacts in the segment receive it; suppressed addresses are skipped. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | Resource id UUID. | ## Body parameters JSON object. | Name | Type | Required | Description | |---|---|---|---| | `scheduled_at` | string | No | ISO 8601 or natural language (`in 1 hour`, `tomorrow at 9am`, UTC), within 30 days. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.broadcasts.send('0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19', { scheduledAt: 'tomorrow at 9am', }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Broadcasts.send("0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19", { "scheduled_at": "tomorrow at 9am", }) print(result) ``` ```php tab="PHP" broadcasts->send('0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19', [ 'scheduled_at' => 'tomorrow at 9am', ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Broadcasts.Send(ctx, "0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19", &pidgely.SendBroadcastRequest{ ScheduledAt: "tomorrow at 9am", }) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Broadcasts.send("0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19", { scheduled_at: "tomorrow at 9am" }) puts result ``` ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/broadcasts/0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19/send' \ -H "Authorization: Bearer $PIDGELY_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "scheduled_at": "tomorrow at 9am" }' ``` ## Response `200 OK` ```json { "id": "0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19" } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | Unique id (UUID). UUID. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | | 409 | `broadcast_not_draft` | The broadcast was already sent or scheduled. | --- # Cancel a broadcast > Cancels a broadcast that is scheduled or queued. A broadcast that is already sending cannot be canceled. Source: https://docs.pidgely.com/api-reference/broadcasts/cancel/ `POST https://api.pidgely.com/broadcasts/{id}/cancel` Cancels a broadcast that is `scheduled` or `queued`. A broadcast that is already `sending` cannot be canceled. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | Resource id UUID. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.broadcasts.cancel('0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19'); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Broadcasts.cancel("0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19") print(result) ``` ```php tab="PHP" broadcasts->cancel('0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19'); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Broadcasts.Cancel(ctx, "0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19") if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Broadcasts.cancel("0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19") puts result ``` ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/broadcasts/0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19/cancel' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "object": "broadcast", "id": "0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19" } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | Yes | Object type. | | `id` | string | Yes | Unique id (UUID). UUID. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | | 409 | `broadcast_not_cancelable` | The broadcast is not scheduled or queued. | --- # Duplicate a broadcast > Creates a new draft with the same content and no schedule. Source: https://docs.pidgely.com/api-reference/broadcasts/duplicate/ `POST https://api.pidgely.com/broadcasts/{id}/duplicate` Creates a new draft with the same content and no schedule. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | Resource id UUID. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.broadcasts.duplicate('0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19'); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Broadcasts.duplicate("0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19") print(result) ``` ```php tab="PHP" broadcasts->duplicate('0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19'); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Broadcasts.Duplicate(ctx, "0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19") if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Broadcasts.duplicate("0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19") puts result ``` ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/broadcasts/0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19/duplicate' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "object": "broadcast", "id": "0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19" } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | Yes | Object type. | | `id` | string | Yes | Unique id (UUID). UUID. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Add a suppression > Adds an address to the suppression list with origin manual. Emails to a suppressed address are accepted but not delivered: the recipient ends as suppressed and does not count toward your quota.… Source: https://docs.pidgely.com/api-reference/suppressions/create/ `POST https://api.pidgely.com/suppressions` Adds an address to the suppression list with origin `manual`. Emails to a suppressed address are accepted but not delivered: the recipient ends as `suppressed` and does not count toward your quota. Adding an address that is already suppressed is a no-op. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Body parameters JSON object. | Name | Type | Required | Description | |---|---|---|---| | `email` | string | Yes | Email address. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.suppressions.add({ email: 'bounced@example.com', }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Suppressions.add({ "email": "bounced@example.com", }) print(result) ``` ```php tab="PHP" suppressions->add([ 'email' => 'bounced@example.com', ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Suppressions.Add(ctx, &pidgely.AddSuppressionRequest{ Email: "bounced@example.com", }) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Suppressions.add({ email: "bounced@example.com" }) puts result ``` ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/suppressions' \ -H "Authorization: Bearer $PIDGELY_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "email": "bounced@example.com" }' ``` ## Response `200 OK` ```json { "object": "suppression", "id": "0199d7f3-2e4a-7f1b-8c6d-3a5e7b9c0d14" } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | Yes | Object type. | | `id` | string | Yes | Unique id (UUID). UUID. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # List suppressions > Lists suppressed addresses, newest first. Filter by origin: bounce, complaint, manual or unsubscribe. Source: https://docs.pidgely.com/api-reference/suppressions/list/ `GET https://api.pidgely.com/suppressions` Lists suppressed addresses, newest first. Filter by `origin`: `bounce`, `complaint`, `manual` or `unsubscribe`. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `origin` | string | No | One of `bounce`, `complaint`, `manual`, `unsubscribe`. | | `limit` | integer | No | Page size (1-100). Between 1 and 100. | | `after` | string | No | Id of the last item of the previous page (older items). UUID. | | `before` | string | No | Id of the first item of the current page (newer items). UUID. | | `q` | string | No | Prefix of the e-mail address (Pidgely extension). | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.suppressions.list({ origin: 'bounce', }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Suppressions.list({ "origin": "bounce", }) print(result) ``` ```php tab="PHP" suppressions->list([ 'origin' => 'bounce', ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Suppressions.List(ctx, &pidgely.ListSuppressionsParams{ Origin: "bounce", }) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Suppressions.list({ origin: "bounce" }) puts result ``` ```bash tab="cURL" curl -X GET 'https://api.pidgely.com/suppressions?origin=bounce' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "object": "list", "has_more": false, "data": [ { "object": "suppression", "id": "0199d7f3-2e4a-7f1b-8c6d-3a5e7b9c0d14", "email": "bounced@example.com", "origin": "bounce", "source_id": "0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21", "created_at": "2026-10-12 16:44:41.000000+00" } ] } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | Yes | Object type. One of `list`. | | `has_more` | boolean | Yes | Whether another page exists. | | `data` | object[] | Yes | Items of this page. | | `data[].object` | string | No | Object type. | | `data[].id` | string | No | Unique id (UUID). UUID. | | `data[].email` | string | No | Email address. | | `data[].origin` | string | No | One of `bounce`, `complaint`, `manual`, `unsubscribe`. | | `data[].source_id` | string \| null | No | UUID. | | `data[].created_at` | string | No | Timestamp in UTC (`2026-10-12 16:44:41.000000+00`). | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Retrieve a suppression > Retrieves a suppression by its id or by the email address. Source: https://docs.pidgely.com/api-reference/suppressions/retrieve/ `GET https://api.pidgely.com/suppressions/{id}` Retrieves a suppression by its id or by the email address. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | Suppression id or e-mail address | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.suppressions.get('bounced@example.com'); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Suppressions.get("bounced@example.com") print(result) ``` ```php tab="PHP" suppressions->get('bounced@example.com'); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Suppressions.Get(ctx, "bounced@example.com") if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Suppressions.get("bounced@example.com") puts result ``` ```bash tab="cURL" curl -X GET 'https://api.pidgely.com/suppressions/bounced@example.com' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "object": "suppression", "id": "0199d7f3-2e4a-7f1b-8c6d-3a5e7b9c0d14", "email": "bounced@example.com", "origin": "bounce", "source_id": "0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21", "created_at": "2026-10-12 16:44:41.000000+00" } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | No | Object type. | | `id` | string | No | Unique id (UUID). UUID. | | `email` | string | No | Email address. | | `origin` | string | No | One of `bounce`, `complaint`, `manual`, `unsubscribe`. | | `source_id` | string \| null | No | UUID. | | `created_at` | string | No | Timestamp in UTC (`2026-10-12 16:44:41.000000+00`). | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Remove a suppression > Removes a suppression by id or email, so the address can receive email again. Removing an unsubscribe suppression also marks the contact as subscribed. Only remove bounce or complaint suppressions… Source: https://docs.pidgely.com/api-reference/suppressions/delete/ `DELETE https://api.pidgely.com/suppressions/{id}` Removes a suppression by id or email, so the address can receive email again. Removing an `unsubscribe` suppression also marks the contact as subscribed. Only remove bounce or complaint suppressions when you know the address is valid and wants your mail. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | Suppression id or e-mail address | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.suppressions.remove('bounced@example.com'); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Suppressions.remove("bounced@example.com") print(result) ``` ```php tab="PHP" suppressions->remove('bounced@example.com'); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Suppressions.Remove(ctx, "bounced@example.com") if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Suppressions.remove("bounced@example.com") puts result ``` ```bash tab="cURL" curl -X DELETE 'https://api.pidgely.com/suppressions/bounced@example.com' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "object": "suppression", "id": "0199d7f3-2e4a-7f1b-8c6d-3a5e7b9c0d14", "deleted": true } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | Yes | Object type. | | `id` | string | Yes | Unique id (UUID). UUID. | | `deleted` | boolean | Yes | — | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Add suppressions in batch > Suppresses 1 to 100 addresses. The batch is not atomic: it stops at the first invalid address and keeps the ones added before it. Source: https://docs.pidgely.com/api-reference/suppressions/batch-add/ `POST https://api.pidgely.com/suppressions/batch/add` Suppresses 1 to 100 addresses. The batch is not atomic: it stops at the first invalid address and keeps the ones added before it. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Body parameters JSON object. | Name | Type | Required | Description | |---|---|---|---| | `emails` | string[] | Yes | Email addresses (1 to 100). | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.suppressions.batch.add({ emails: ['a@example.com', 'b@example.com'], }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Suppressions.Batch.add({ "emails": ["a@example.com", "b@example.com"], }) print(result) ``` ```php tab="PHP" suppressions->batch->add([ 'emails' => ['a@example.com', 'b@example.com'], ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Suppressions.Batch.Add(ctx, &pidgely.AddSuppressionBatchRequest{ Emails: []string{"a@example.com", "b@example.com"}, }) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Suppressions::Batch.add({ emails: ["a@example.com", "b@example.com"] }) puts result ``` ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/suppressions/batch/add' \ -H "Authorization: Bearer $PIDGELY_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "emails": [ "a@example.com", "b@example.com" ] }' ``` ## Response `200 OK` ```json { "data": [ { "object": "suppression", "id": "0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21" } ] } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `data` | object[] | No | Items of this page. | | `data[].object` | string | Yes | Object type. | | `data[].id` | string | Yes | Unique id (UUID). UUID. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Remove suppressions in batch > Removes up to 100 suppressions by ids and/or emails. Entries that do not exist are skipped. Removing an unsubscribe suppression re-subscribes the matching contact. Source: https://docs.pidgely.com/api-reference/suppressions/batch-remove/ `POST https://api.pidgely.com/suppressions/batch/remove` Removes up to 100 suppressions by `ids` and/or `emails`. Entries that do not exist are skipped. Removing an `unsubscribe` suppression re-subscribes the matching contact. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Body parameters JSON object. | Name | Type | Required | Description | |---|---|---|---| | `emails` | string[] | No | Email addresses (1 to 100). | | `ids` | string[] | No | Suppression ids. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.suppressions.batch.remove({ emails: ['a@example.com'], ids: ['0199d7f3-2e4a-7f1b-8c6d-3a5e7b9c0d14'], }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Suppressions.Batch.remove({ "emails": ["a@example.com"], "ids": ["0199d7f3-2e4a-7f1b-8c6d-3a5e7b9c0d14"], }) print(result) ``` ```php tab="PHP" suppressions->batch->remove([ 'emails' => ['a@example.com'], 'ids' => ['0199d7f3-2e4a-7f1b-8c6d-3a5e7b9c0d14'], ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Suppressions.Batch.Remove(ctx, &pidgely.RemoveSuppressionBatchRequest{ Emails: []string{"a@example.com"}, Ids: []string{"0199d7f3-2e4a-7f1b-8c6d-3a5e7b9c0d14"}, }) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Suppressions::Batch.remove({ emails: ["a@example.com"], ids: ["0199d7f3-2e4a-7f1b-8c6d-3a5e7b9c0d14"] }) puts result ``` ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/suppressions/batch/remove' \ -H "Authorization: Bearer $PIDGELY_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "emails": [ "a@example.com" ], "ids": [ "0199d7f3-2e4a-7f1b-8c6d-3a5e7b9c0d14" ] }' ``` ## Response `200 OK` ```json { "data": [ { "object": "suppression", "id": "0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21", "deleted": true } ] } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `data` | object[] | No | Items of this page. | | `data[].object` | string | Yes | Object type. | | `data[].id` | string | Yes | Unique id (UUID). UUID. | | `data[].deleted` | boolean | Yes | — | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Create an audience > Legacy alias of Create a segment, kept for existing code that uses audiences. Source: https://docs.pidgely.com/api-reference/audiences/create/ `POST https://api.pidgely.com/audiences` Legacy alias of [Create a segment](https://docs.pidgely.com/api-reference/segments/create/), kept for existing code that uses audiences. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Body parameters JSON object. | Name | Type | Required | Description | |---|---|---|---| | `name` | string | Yes | Display name. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.audiences.create({ name: 'Newsletter', }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Audiences.create({ "name": "Newsletter", }) print(result) ``` ```php tab="PHP" audiences->create([ 'name' => 'Newsletter', ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Audiences.Create(ctx, &pidgely.CreateSegmentRequest{ Name: "Newsletter", }) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Audiences.create({ name: "Newsletter" }) puts result ``` ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/audiences' \ -H "Authorization: Bearer $PIDGELY_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "name": "Newsletter" }' ``` ## Response `200 OK` ```json { "object": "audience", "id": "0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21", "name": "Example" } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | No | Object type. | | `id` | string | No | Unique id (UUID). UUID. | | `name` | string | No | Display name. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # List audiences > Legacy alias of List segments. Source: https://docs.pidgely.com/api-reference/audiences/list/ `GET https://api.pidgely.com/audiences` Legacy alias of [List segments](https://docs.pidgely.com/api-reference/segments/list/). **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `limit` | integer | No | Page size (1-100). Between 1 and 100. | | `after` | string | No | Id of the last item of the previous page (older items). UUID. | | `before` | string | No | Id of the first item of the current page (newer items). UUID. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.audiences.list(); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Audiences.list() print(result) ``` ```php tab="PHP" audiences->list(); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Audiences.List(ctx, nil) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Audiences.list() puts result ``` ```bash tab="cURL" curl -X GET 'https://api.pidgely.com/audiences' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "object": "list", "has_more": false, "data": [ { "object": "audience", "id": "0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21", "name": "Example", "created_at": "2026-10-12 16:44:41.000000+00", "contacts_count": 1 } ] } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | Yes | Object type. One of `list`. | | `has_more` | boolean | Yes | Whether another page exists. | | `data` | object[] | Yes | Items of this page. | | `data[].object` | string | No | Object type. | | `data[].id` | string | No | Unique id (UUID). UUID. | | `data[].name` | string | No | Display name. | | `data[].created_at` | string | No | Timestamp in UTC (`2026-10-12 16:44:41.000000+00`). | | `data[].contacts_count` | integer | No | Number of contacts in the segment. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Retrieve an audience > Legacy alias of Retrieve a segment. Source: https://docs.pidgely.com/api-reference/audiences/retrieve/ `GET https://api.pidgely.com/audiences/{id}` Legacy alias of [Retrieve a segment](https://docs.pidgely.com/api-reference/segments/retrieve/). **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | Resource id UUID. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.audiences.get('0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48'); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Audiences.get("0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48") print(result) ``` ```php tab="PHP" audiences->get('0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48'); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Audiences.Get(ctx, "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48") if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Audiences.get("0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48") puts result ``` ```bash tab="cURL" curl -X GET 'https://api.pidgely.com/audiences/0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "object": "audience", "id": "0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21", "name": "Example", "created_at": "2026-10-12 16:44:41.000000+00", "contacts_count": 1 } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | No | Object type. | | `id` | string | No | Unique id (UUID). UUID. | | `name` | string | No | Display name. | | `created_at` | string | No | Timestamp in UTC (`2026-10-12 16:44:41.000000+00`). | | `contacts_count` | integer | No | Number of contacts in the segment. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Delete an audience > Legacy alias of Delete a segment. Source: https://docs.pidgely.com/api-reference/audiences/delete/ `DELETE https://api.pidgely.com/audiences/{id}` Legacy alias of [Delete a segment](https://docs.pidgely.com/api-reference/segments/delete/). **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | Resource id UUID. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.audiences.remove('0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48'); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Audiences.remove("0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48") print(result) ``` ```php tab="PHP" audiences->remove('0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48'); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Audiences.Remove(ctx, "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48") if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Audiences.remove("0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48") puts result ``` ```bash tab="cURL" curl -X DELETE 'https://api.pidgely.com/audiences/0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "object": "audience", "id": "0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21", "deleted": true } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | Yes | Object type. | | `id` | string | Yes | Unique id (UUID). UUID. | | `deleted` | boolean | Yes | — | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # List an audience's contacts > Lists the contacts in the audience (segment). Source: https://docs.pidgely.com/api-reference/audiences/list-contacts/ `GET https://api.pidgely.com/audiences/{id}/contacts` Lists the contacts in the audience (segment). **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | Resource id UUID. | ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `limit` | integer | No | Page size (1-100). Between 1 and 100. | | `after` | string | No | Id of the last item of the previous page (older items). UUID. | | `before` | string | No | Id of the first item of the current page (newer items). UUID. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.contacts.list({ audienceId: '0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48', }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Contacts.list({ "audience_id": "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48", }) print(result) ``` ```php tab="PHP" contacts->list([ 'audience_id' => '0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48', ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Contacts.List(ctx, &pidgely.ListContactsParams{ AudienceId: "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48", }) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Contacts.list({ audience_id: "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48" }) puts result ``` ```bash tab="cURL" curl -X GET 'https://api.pidgely.com/audiences/0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48/contacts' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "object": "list", "has_more": false, "data": [ { "object": "audience", "id": "0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21", "email": "steve@example.com", "first_name": "Steve", "last_name": "Wozniak", "created_at": "2026-10-12 16:44:41.000000+00", "unsubscribed": false, "properties": {} } ] } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | Yes | Object type. One of `list`. | | `has_more` | boolean | Yes | Whether another page exists. | | `data` | object[] | Yes | Items of this page. | | `data[].object` | string | No | Object type. | | `data[].id` | string | No | Unique id (UUID). UUID. | | `data[].email` | string | No | Email address. | | `data[].first_name` | string | No | First name, up to 200 characters. Available as `{{{FIRST_NAME}}}` in broadcasts. | | `data[].last_name` | string | No | Last name, up to 200 characters. Available as `{{{LAST_NAME}}}` in broadcasts. | | `data[].created_at` | string | No | Timestamp in UTC (`2026-10-12 16:44:41.000000+00`). | | `data[].unsubscribed` | boolean | No | Whether the contact opted out of broadcasts. Kept in sync with the suppression list. | | `data[].properties` | object (map of object) | No | Custom properties: up to 100 keys matching `^[A-Za-z_][A-Za-z0-9_]{0,49}$`, values string (up to 2,000 characters), number, boolean, or `null` to delete. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Create a contact in an audience > Creates (or updates) the contact and adds it to the audience (segment). Source: https://docs.pidgely.com/api-reference/audiences/add-contact/ `POST https://api.pidgely.com/audiences/{id}/contacts` Creates (or updates) the contact and adds it to the audience (segment). **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | Resource id UUID. | ## Body parameters JSON object. | Name | Type | Required | Description | |---|---|---|---| | `email` | string | No | Email address. | | `first_name` | string | No | First name, up to 200 characters. Available as `{{{FIRST_NAME}}}` in broadcasts. | | `last_name` | string | No | Last name, up to 200 characters. Available as `{{{LAST_NAME}}}` in broadcasts. | | `unsubscribed` | boolean | No | Whether the contact opted out of broadcasts. Kept in sync with the suppression list. | | `properties` | object (map of string \| number \| boolean \| null) | No | Custom properties: up to 100 keys matching `^[A-Za-z_][A-Za-z0-9_]{0,49}$`, values string (up to 2,000 characters), number, boolean, or `null` to delete. | | `segments` | object[] | No | Segments to add the contact to. | | `segments[].id` | string | Yes | Segment id. UUID. | | `topics` | any[] | No | Not supported yet. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.contacts.create({ audienceId: '0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48', email: 'steve@example.com', firstName: 'Steve', unsubscribed: false, }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Contacts.create({ "audience_id": "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48", "email": "steve@example.com", "first_name": "Steve", "unsubscribed": False, }) print(result) ``` ```php tab="PHP" contacts->create([ 'audience_id' => '0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48', 'email' => 'steve@example.com', 'first_name' => 'Steve', 'unsubscribed' => false, ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Contacts.Create(ctx, &pidgely.CreateContactRequest{ Email: "steve@example.com", FirstName: "Steve", Unsubscribed: pidgely.Bool(false), AudienceId: "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48", }) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Contacts.create({ audience_id: "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48", email: "steve@example.com", first_name: "Steve", unsubscribed: false }) puts result ``` ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/audiences/0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48/contacts' \ -H "Authorization: Bearer $PIDGELY_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "email": "steve@example.com", "first_name": "Steve", "unsubscribed": false }' ``` ## Response `200 OK` ```json { "object": "contact", "id": "0199d7f4-5d7b-7a2c-b4e1-9f3c2a6d8e05" } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | Yes | Object type. | | `id` | string | Yes | Unique id (UUID). UUID. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Retrieve a contact > Retrieves the contact by id or email. Contacts are global to the account, so the audience id is not used to filter. Source: https://docs.pidgely.com/api-reference/audiences/retrieve-contact/ `GET https://api.pidgely.com/audiences/{id}/contacts/{key}` Retrieves the contact by id or email. Contacts are global to the account, so the audience id is not used to filter. **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | Resource id UUID. | | `key` | string | Yes | Contact id or e-mail address | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.contacts.get({ email: 'steve@example.com', audienceId: '0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48', }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Contacts.get({ "email": "steve@example.com", "audience_id": "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48", }) print(result) ``` ```php tab="PHP" contacts->get([ 'email' => 'steve@example.com', 'audience_id' => '0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48', ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Contacts.Get(ctx, "steve@example.com") if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Contacts.get({ email: "steve@example.com", audience_id: "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48" }) puts result ``` ```bash tab="cURL" curl -X GET 'https://api.pidgely.com/audiences/0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48/contacts/steve@example.com' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "object": "audience", "id": "0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21", "email": "steve@example.com", "first_name": "Steve", "last_name": "Wozniak", "created_at": "2026-10-12 16:44:41.000000+00", "unsubscribed": false, "properties": {} } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | No | Object type. | | `id` | string | No | Unique id (UUID). UUID. | | `email` | string | No | Email address. | | `first_name` | string | No | First name, up to 200 characters. Available as `{{{FIRST_NAME}}}` in broadcasts. | | `last_name` | string | No | Last name, up to 200 characters. Available as `{{{LAST_NAME}}}` in broadcasts. | | `created_at` | string | No | Timestamp in UTC (`2026-10-12 16:44:41.000000+00`). | | `unsubscribed` | boolean | No | Whether the contact opted out of broadcasts. Kept in sync with the suppression list. | | `properties` | object (map of object) | No | Custom properties: up to 100 keys matching `^[A-Za-z_][A-Za-z0-9_]{0,49}$`, values string (up to 2,000 characters), number, boolean, or `null` to delete. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Update a contact > Updates the contact by id or email (same as Update a contact). Source: https://docs.pidgely.com/api-reference/audiences/update-contact/ `PATCH https://api.pidgely.com/audiences/{id}/contacts/{key}` Updates the contact by id or email (same as [Update a contact](https://docs.pidgely.com/api-reference/contacts/update/)). **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | Resource id UUID. | | `key` | string | Yes | Contact id or e-mail address | ## Body parameters JSON object. | Name | Type | Required | Description | |---|---|---|---| | `email` | string | No | Email address. | | `first_name` | string | No | First name, up to 200 characters. Available as `{{{FIRST_NAME}}}` in broadcasts. | | `last_name` | string | No | Last name, up to 200 characters. Available as `{{{LAST_NAME}}}` in broadcasts. | | `unsubscribed` | boolean | No | Whether the contact opted out of broadcasts. Kept in sync with the suppression list. | | `properties` | object (map of string \| number \| boolean \| null) | No | Custom properties: up to 100 keys matching `^[A-Za-z_][A-Za-z0-9_]{0,49}$`, values string (up to 2,000 characters), number, boolean, or `null` to delete. | | `segments` | object[] | No | Segments to add the contact to. | | `segments[].id` | string | Yes | Segment id. UUID. | | `topics` | any[] | No | Not supported yet. | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.contacts.update({ email: 'steve@example.com', audienceId: '0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48', unsubscribed: true, }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Contacts.update({ "email": "steve@example.com", "audience_id": "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48", "unsubscribed": True, }) print(result) ``` ```php tab="PHP" contacts->update([ 'email' => 'steve@example.com', 'audience_id' => '0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48', 'unsubscribed' => true, ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Contacts.Update(ctx, "steve@example.com", &pidgely.UpdateContactRequest{ Unsubscribed: pidgely.Bool(true), AudienceId: "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48", }) if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Contacts.update({ email: "steve@example.com", audience_id: "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48", unsubscribed: true }) puts result ``` ```bash tab="cURL" curl -X PATCH 'https://api.pidgely.com/audiences/0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48/contacts/steve@example.com' \ -H "Authorization: Bearer $PIDGELY_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "unsubscribed": true }' ``` ## Response `200 OK` ```json { "object": "audience", "id": "0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21" } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | Yes | Object type. | | `id` | string | Yes | Unique id (UUID). UUID. | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Delete a contact > DELETE /audiences/{id}/contacts/{key} Source: https://docs.pidgely.com/api-reference/audiences/remove-contact/ `DELETE https://api.pidgely.com/audiences/{id}/contacts/{key}` > [!WARNING] > This deletes the **whole contact**, not only its membership in the audience, as contacts are global to the account. To remove a contact from one segment only, use [Remove from segment](https://docs.pidgely.com/api-reference/contacts/remove-segment/). **Authorization:** `Bearer pdg_…`. An API key with `full_access`. A `sending_access` key gets `401 restricted_api_key`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | Resource id UUID. | | `key` | string | Yes | Contact id or e-mail address | ## Request ```js tab="Node.js" import { Pidgely } from 'pidgely'; const pidgely = new Pidgely(process.env.PIDGELY_API_KEY); const { data, error } = await pidgely.contacts.remove({ email: 'steve@example.com', audienceId: '0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48', }); console.log(data ?? error); ``` ```python tab="Python" import os import pidgely pidgely.api_key = os.environ["PIDGELY_API_KEY"] result = pidgely.Contacts.remove({ "email": "steve@example.com", "audience_id": "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48", }) print(result) ``` ```php tab="PHP" contacts->remove([ 'email' => 'steve@example.com', 'audience_id' => '0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48', ]); var_dump($result); ``` ```go tab="Go" package main import ( "context" "fmt" "os" "github.com/pidgely/pidgely-go" ) func main() { ctx := context.Background() client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY")) result, err := client.Contacts.Remove(ctx, "steve@example.com") if err != nil { panic(err) } fmt.Println(result) } ``` ```ruby tab="Ruby" require "pidgely" Pidgely.api_key = ENV["PIDGELY_API_KEY"] result = Pidgely::Contacts.remove({ email: "steve@example.com", audience_id: "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48" }) puts result ``` ```bash tab="cURL" curl -X DELETE 'https://api.pidgely.com/audiences/0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48/contacts/steve@example.com' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "object": "audience", "contact": "0199d7f4-5d7b-7a2c-b4e1-9f3c2a6d8e05", "deleted": true } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `object` | string | No | Object type. | | `contact` | string | No | UUID. | | `deleted` | boolean | No | — | ## Errors Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list. | Status | Name | When | |---|---|---| | 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. | | 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). | | 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). | | 422 | `validation_error` | A field is missing or invalid; `message` names it. | | 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. | --- # Create an API key > Creates a key with scope full or sending, optionally restricted to one domain_id. The token is returned once. Source: https://docs.pidgely.com/api-reference/v1/api-keys/create/ `POST https://api.pidgely.com/v1/api-keys` Creates a key with `scope` `full` or `sending`, optionally restricted to one `domain_id`. The token is returned once. **Authorization:** `Bearer pdg_…`. An API key with the `full` scope. ## Body parameters JSON object. | Name | Type | Required | Description | |---|---|---|---| | `name` | string | Yes | Display name. | | `scope` | string | No | `full` or `sending`. One of `full`, `sending`. | | `domain_id` | string | No | Restricts the key to sending from this domain (implies `sending_access`). UUID. | ## Request ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/v1/api-keys' \ -H "Authorization: Bearer $PIDGELY_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "name": "Production", "scope": "sending" }' ``` ## Response `201 Created` with a JSON object. ## Errors Native routes return `{"error": {"code", "message", "field", "request_id"}}`. See [Errors](https://docs.pidgely.com/errors/#native-api-errors). | Status | Code | When | |---|---|---| | 401 | `missing_api_key` | No `Authorization: Bearer` header. | | 403 | `invalid_api_key`, `insufficient_scope` | The key is invalid or lacks the scope. | | 404 | `not_found` | The resource does not exist in this account. | | 422 | `validation_error` | A field is missing or invalid; `field` names it. | | 429 | `rate_limited`, `daily_quota_exceeded` | Too many requests, or the quota is used up. | --- # List API keys > Lists API keys (never the tokens). Source: https://docs.pidgely.com/api-reference/v1/api-keys/list/ `GET https://api.pidgely.com/v1/api-keys` Lists API keys (never the tokens). **Authorization:** `Bearer pdg_…`. An API key with the `full` scope. ## Request ```bash tab="cURL" curl -X GET 'https://api.pidgely.com/v1/api-keys' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` with a JSON object. ## Errors Native routes return `{"error": {"code", "message", "field", "request_id"}}`. See [Errors](https://docs.pidgely.com/errors/#native-api-errors). | Status | Code | When | |---|---|---| | 401 | `missing_api_key` | No `Authorization: Bearer` header. | | 403 | `invalid_api_key`, `insufficient_scope` | The key is invalid or lacks the scope. | | 404 | `not_found` | The resource does not exist in this account. | | 422 | `validation_error` | A field is missing or invalid; `field` names it. | | 429 | `rate_limited`, `daily_quota_exceeded` | Too many requests, or the quota is used up. | --- # Revoke an API key > Revokes a key. Returns 204 No Content. Source: https://docs.pidgely.com/api-reference/v1/api-keys/delete/ `DELETE https://api.pidgely.com/v1/api-keys/{id}` Revokes a key. Returns `204 No Content`. **Authorization:** `Bearer pdg_…`. An API key with the `full` scope. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | Resource id UUID. | ## Request ```bash tab="cURL" curl -X DELETE 'https://api.pidgely.com/v1/api-keys/0199d7f1-3a5c-7b2e-9d4f-6e1a0c8b7d92' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `204 No Content` with an empty body. ## Errors Native routes return `{"error": {"code", "message", "field", "request_id"}}`. See [Errors](https://docs.pidgely.com/errors/#native-api-errors). | Status | Code | When | |---|---|---| | 401 | `missing_api_key` | No `Authorization: Bearer` header. | | 403 | `invalid_api_key`, `insufficient_scope` | The key is invalid or lacks the scope. | | 404 | `not_found` | The resource does not exist in this account. | | 422 | `validation_error` | A field is missing or invalid; `field` names it. | | 429 | `rate_limited`, `daily_quota_exceeded` | Too many requests, or the quota is used up. | --- # Create a domain > Adds a sending domain and returns it with the DNS records to publish. Source: https://docs.pidgely.com/api-reference/v1/domains/create/ `POST https://api.pidgely.com/v1/domains` Adds a sending domain and returns it with the DNS records to publish. **Authorization:** `Bearer pdg_…`. An API key with the `full` scope. ## Body parameters JSON object. | Name | Type | Required | Description | |---|---|---|---| | `name` | string | Yes | Display name. | | `region` | string | No | Informative; set by the platform. | | `custom_return_path` | string | No | Bounce subdomain label (default `bounces`). | | `tls` | string | No | `opportunistic` (default): TLS when the receiving server offers it. `enforced`: deliver only over TLS. One of `opportunistic`, `enforced`. | | `open_tracking` | boolean | No | Accepted, not supported yet (always false). | | `click_tracking` | boolean | No | Accepted, not supported yet. | | `capabilities` | object | No | — | | `capabilities.sending` | string | No | — | | `capabilities.receiving` | string | No | `enabled` is rejected (receiving not supported yet). | ## Request ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/v1/domains' \ -H "Authorization: Bearer $PIDGELY_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "name": "acme.com" }' ``` ## Response `201 Created` with a JSON object. ## Errors Native routes return `{"error": {"code", "message", "field", "request_id"}}`. See [Errors](https://docs.pidgely.com/errors/#native-api-errors). | Status | Code | When | |---|---|---| | 401 | `missing_api_key` | No `Authorization: Bearer` header. | | 403 | `invalid_api_key`, `insufficient_scope` | The key is invalid or lacks the scope. | | 404 | `not_found` | The resource does not exist in this account. | | 422 | `validation_error` | A field is missing or invalid; `field` names it. | | 429 | `rate_limited`, `daily_quota_exceeded` | Too many requests, or the quota is used up. | --- # List domains > Lists domains. Source: https://docs.pidgely.com/api-reference/v1/domains/list/ `GET https://api.pidgely.com/v1/domains` Lists domains. **Authorization:** `Bearer pdg_…`. An API key with the `full` scope. ## Request ```bash tab="cURL" curl -X GET 'https://api.pidgely.com/v1/domains' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` with a JSON object. ## Errors Native routes return `{"error": {"code", "message", "field", "request_id"}}`. See [Errors](https://docs.pidgely.com/errors/#native-api-errors). | Status | Code | When | |---|---|---| | 401 | `missing_api_key` | No `Authorization: Bearer` header. | | 403 | `invalid_api_key`, `insufficient_scope` | The key is invalid or lacks the scope. | | 404 | `not_found` | The resource does not exist in this account. | | 422 | `validation_error` | A field is missing or invalid; `field` names it. | | 429 | `rate_limited`, `daily_quota_exceeded` | Too many requests, or the quota is used up. | --- # Retrieve a domain > Retrieves a domain with its per-record checks (dkim, spf, return_path, dmarc). Source: https://docs.pidgely.com/api-reference/v1/domains/retrieve/ `GET https://api.pidgely.com/v1/domains/{id}` Retrieves a domain with its per-record checks (`dkim`, `spf`, `return_path`, `dmarc`). **Authorization:** `Bearer pdg_…`. An API key with the `full` scope. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | Resource id UUID. | ## Request ```bash tab="cURL" curl -X GET 'https://api.pidgely.com/v1/domains/0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` with a JSON object. ## Errors Native routes return `{"error": {"code", "message", "field", "request_id"}}`. See [Errors](https://docs.pidgely.com/errors/#native-api-errors). | Status | Code | When | |---|---|---| | 401 | `missing_api_key` | No `Authorization: Bearer` header. | | 403 | `invalid_api_key`, `insufficient_scope` | The key is invalid or lacks the scope. | | 404 | `not_found` | The resource does not exist in this account. | | 422 | `validation_error` | A field is missing or invalid; `field` names it. | | 429 | `rate_limited`, `daily_quota_exceeded` | Too many requests, or the quota is used up. | --- # Delete a domain > Deletes a domain. Returns 204 No Content. Source: https://docs.pidgely.com/api-reference/v1/domains/delete/ `DELETE https://api.pidgely.com/v1/domains/{id}` Deletes a domain. Returns `204 No Content`. **Authorization:** `Bearer pdg_…`. An API key with the `full` scope. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | Resource id UUID. | ## Request ```bash tab="cURL" curl -X DELETE 'https://api.pidgely.com/v1/domains/0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `204 No Content` with an empty body. ## Errors Native routes return `{"error": {"code", "message", "field", "request_id"}}`. See [Errors](https://docs.pidgely.com/errors/#native-api-errors). | Status | Code | When | |---|---|---| | 401 | `missing_api_key` | No `Authorization: Bearer` header. | | 403 | `invalid_api_key`, `insufficient_scope` | The key is invalid or lacks the scope. | | 404 | `not_found` | The resource does not exist in this account. | | 422 | `validation_error` | A field is missing or invalid; `field` names it. | | 429 | `rate_limited`, `daily_quota_exceeded` | Too many requests, or the quota is used up. | --- # Verify DNS now > Checks the DNS records now and returns the updated domain. Source: https://docs.pidgely.com/api-reference/v1/domains/verify/ `POST https://api.pidgely.com/v1/domains/{id}/verify` Checks the DNS records now and returns the updated domain. **Authorization:** `Bearer pdg_…`. An API key with the `full` scope. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | Resource id UUID. | ## Request ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/v1/domains/0199d7f0-1c2b-7e4d-a5f6-0b9c8d7e6f51/verify' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` with a JSON object. ## Errors Native routes return `{"error": {"code", "message", "field", "request_id"}}`. See [Errors](https://docs.pidgely.com/errors/#native-api-errors). | Status | Code | When | |---|---|---| | 401 | `missing_api_key` | No `Authorization: Bearer` header. | | 403 | `invalid_api_key`, `insufficient_scope` | The key is invalid or lacks the scope. | | 404 | `not_found` | The resource does not exist in this account. | | 422 | `validation_error` | A field is missing or invalid; `field` names it. | | 429 | `rate_limited`, `daily_quota_exceeded` | Too many requests, or the quota is used up. | --- # Send an email > Native equivalent of Send an email: same body, Idempotency-Key supported, errors in the native shape. Unknown fields are rejected (the main routes ignore them). Source: https://docs.pidgely.com/api-reference/v1/emails/send/ `POST https://api.pidgely.com/v1/emails` Native equivalent of [Send an email](https://docs.pidgely.com/api-reference/emails/send/): same body, `Idempotency-Key` supported, errors in the native shape. Unknown fields are rejected (the main routes ignore them). **Authorization:** `Bearer pdg_…`. Any API key (`full` or `sending` scope). ## Body parameters JSON object. | Name | Type | Required | Description | |---|---|---|---| | `from` | string | No | `Name `; may come from the template. | | `to` | string \| string[] | Yes | Recipient address (`a@x.com` or `Name `) or a list of them. Up to 50 unique recipients across `to`, `cc` and `bcc`. | | `cc` | string \| string[] | No | Carbon-copy recipients. Count toward the 50-recipient limit. | | `bcc` | string \| string[] | No | Blind carbon-copy recipients. Count toward the 50-recipient limit. | | `reply_to` | string \| string[] | No | Reply-To address or list of addresses. | | `subject` | string | No | Subject line, up to 998 bytes. Required unless a template provides it. | | `html` | string | No | HTML body. `html` or `text` is required (unless you send with a template). | | `text` | string | No | Generated from html when omitted. | | `headers` | object (map of string) | No | Custom headers. Platform-managed headers (From, To, Subject, Message-ID, DKIM-Signature, Return-Path, Reply-To, …) are rejected; values cannot contain line breaks. | | `attachments` | object[] | No | Files to attach. All attachments share a 40 MiB budget (decoded). Executable types (`.exe`, `.js`, `.bat`, …) are blocked. | | `attachments[].filename` | string | No | File name shown to the recipient. Defaults to the last segment of `path`. | | `attachments[].content` | string | No | Base64 content. Use either content or path. | | `attachments[].path` | string | No | http(s) URL downloaded at send time (SSRF-guarded, 40MB budget, no redirects). | | `attachments[].content_type` | string | No | MIME type. Guessed from the file extension when omitted. | | `attachments[].content_id` | string | No | Makes the attachment inline (`cid:` in HTML). | | `tags` | object[] | No | Name/value pairs attached to the email and returned in webhooks. | | `tags[].name` | string | Yes | Display name. Pattern: `^[A-Za-z0-9_-]{1,256}$`. | | `tags[].value` | string | Yes | Pattern: `^[A-Za-z0-9_-]{1,256}$`. | | `scheduled_at` | string | No | ISO 8601 or natural language (`in 1 hour`, `tomorrow at 9am`); within 30 days. | | `template` | object | No | — | | `template.id` | string | Yes | Template id or alias (published version is used). | | `template.variables` | object (map of string \| number) | No | Values for the template variables: strings up to 2,000 characters or numbers. | | `topic_id` | string | No | Not supported yet; a non-empty value is rejected. | | `stream` | string | No | Pidgely extension: `transactional` (default) or `broadcast` (adds RFC 8058 unsubscribe). One of `transactional`, `broadcast`. | ## Request ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/v1/emails' \ -H "Authorization: Bearer $PIDGELY_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "from": "Acme ", "to": [ "delivered@example.com" ], "subject": "Welcome to Acme", "html": "

Thanks for signing up!

", "tags": [ { "name": "category", "value": "welcome" } ] }' ``` ## Response `200 OK` ```json { "id": "0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21" } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | Unique id (UUID). UUID. | ## Errors Native routes return `{"error": {"code", "message", "field", "request_id"}}`. See [Errors](https://docs.pidgely.com/errors/#native-api-errors). | Status | Code | When | |---|---|---| | 401 | `missing_api_key` | No `Authorization: Bearer` header. | | 403 | `invalid_api_key`, `insufficient_scope` | The key is invalid or lacks the scope. | | 404 | `not_found` | The resource does not exist in this account. | | 422 | `validation_error` | A field is missing or invalid; `field` names it. | | 429 | `rate_limited`, `daily_quota_exceeded` | Too many requests, or the quota is used up. | --- # Send up to 100 emails atomically > Sends 1 to 100 emails atomically (all or nothing). Source: https://docs.pidgely.com/api-reference/v1/emails/send-batch/ `POST https://api.pidgely.com/v1/emails/batch` Sends 1 to 100 emails atomically (all or nothing). **Authorization:** `Bearer pdg_…`. Any API key (`full` or `sending` scope). ## Body parameters A JSON array of objects (1–100 items). Each item: | Name | Type | Required | Description | |---|---|---|---| | `from` | string | No | `Name `; may come from the template. | | `to` | string \| string[] | Yes | Recipient address (`a@x.com` or `Name `) or a list of them. Up to 50 unique recipients across `to`, `cc` and `bcc`. | | `cc` | string \| string[] | No | Carbon-copy recipients. Count toward the 50-recipient limit. | | `bcc` | string \| string[] | No | Blind carbon-copy recipients. Count toward the 50-recipient limit. | | `reply_to` | string \| string[] | No | Reply-To address or list of addresses. | | `subject` | string | No | Subject line, up to 998 bytes. Required unless a template provides it. | | `html` | string | No | HTML body. `html` or `text` is required (unless you send with a template). | | `text` | string | No | Generated from html when omitted. | | `headers` | object (map of string) | No | Custom headers. Platform-managed headers (From, To, Subject, Message-ID, DKIM-Signature, Return-Path, Reply-To, …) are rejected; values cannot contain line breaks. | | `attachments` | object[] | No | Files to attach. All attachments share a 40 MiB budget (decoded). Executable types (`.exe`, `.js`, `.bat`, …) are blocked. | | `attachments[].filename` | string | No | File name shown to the recipient. Defaults to the last segment of `path`. | | `attachments[].content` | string | No | Base64 content. Use either content or path. | | `attachments[].path` | string | No | http(s) URL downloaded at send time (SSRF-guarded, 40MB budget, no redirects). | | `attachments[].content_type` | string | No | MIME type. Guessed from the file extension when omitted. | | `attachments[].content_id` | string | No | Makes the attachment inline (`cid:` in HTML). | | `tags` | object[] | No | Name/value pairs attached to the email and returned in webhooks. | | `tags[].name` | string | Yes | Display name. Pattern: `^[A-Za-z0-9_-]{1,256}$`. | | `tags[].value` | string | Yes | Pattern: `^[A-Za-z0-9_-]{1,256}$`. | | `scheduled_at` | string | No | ISO 8601 or natural language (`in 1 hour`, `tomorrow at 9am`); within 30 days. | | `template` | object | No | — | | `template.id` | string | Yes | Template id or alias (published version is used). | | `template.variables` | object (map of string \| number) | No | Values for the template variables: strings up to 2,000 characters or numbers. | | `topic_id` | string | No | Not supported yet; a non-empty value is rejected. | | `stream` | string | No | Pidgely extension: `transactional` (default) or `broadcast` (adds RFC 8058 unsubscribe). One of `transactional`, `broadcast`. | ## Request ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/v1/emails/batch' \ -H "Authorization: Bearer $PIDGELY_API_KEY" \ -H 'Content-Type: application/json' \ -d '[ { "from": "Acme ", "to": [ "delivered@example.com" ], "subject": "Welcome to Acme", "html": "

Thanks for signing up!

", "tags": [ { "name": "category", "value": "welcome" } ] } ]' ``` ## Response `200 OK` with a JSON object. ## Errors Native routes return `{"error": {"code", "message", "field", "request_id"}}`. See [Errors](https://docs.pidgely.com/errors/#native-api-errors). | Status | Code | When | |---|---|---| | 401 | `missing_api_key` | No `Authorization: Bearer` header. | | 403 | `invalid_api_key`, `insufficient_scope` | The key is invalid or lacks the scope. | | 404 | `not_found` | The resource does not exist in this account. | | 422 | `validation_error` | A field is missing or invalid; `field` names it. | | 429 | `rate_limited`, `daily_quota_exceeded` | Too many requests, or the quota is used up. | --- # List emails > Lists emails, newest first. Paginate with before (an email id). Always pass limit (1–100). Source: https://docs.pidgely.com/api-reference/v1/emails/list/ `GET https://api.pidgely.com/v1/emails` Lists emails, newest first. Paginate with `before` (an email id). Always pass `limit` (1–100). **Authorization:** `Bearer pdg_…`. An API key with the `full` scope. ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `limit` | integer | No | — | | `before` | string | No | UUID. | ## Request ```bash tab="cURL" curl -X GET 'https://api.pidgely.com/v1/emails?limit=20' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` with a JSON object. ## Errors Native routes return `{"error": {"code", "message", "field", "request_id"}}`. See [Errors](https://docs.pidgely.com/errors/#native-api-errors). | Status | Code | When | |---|---|---| | 401 | `missing_api_key` | No `Authorization: Bearer` header. | | 403 | `invalid_api_key`, `insufficient_scope` | The key is invalid or lacks the scope. | | 404 | `not_found` | The resource does not exist in this account. | | 422 | `validation_error` | A field is missing or invalid; `field` names it. | | 429 | `rate_limited`, `daily_quota_exceeded` | Too many requests, or the quota is used up. | --- # Retrieve an email with deliveries > Returns the email with one delivery per recipient (status, attempts, last SMTP response) and its event timeline. Source: https://docs.pidgely.com/api-reference/v1/emails/retrieve/ `GET https://api.pidgely.com/v1/emails/{id}` Returns the email with one delivery per recipient (status, attempts, last SMTP response) and its event timeline. **Authorization:** `Bearer pdg_…`. Any API key (`full` or `sending` scope). ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | Resource id UUID. | ## Request ```bash tab="cURL" curl -X GET 'https://api.pidgely.com/v1/emails/0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` with a JSON object. ## Errors Native routes return `{"error": {"code", "message", "field", "request_id"}}`. See [Errors](https://docs.pidgely.com/errors/#native-api-errors). | Status | Code | When | |---|---|---| | 401 | `missing_api_key` | No `Authorization: Bearer` header. | | 403 | `invalid_api_key`, `insufficient_scope` | The key is invalid or lacks the scope. | | 404 | `not_found` | The resource does not exist in this account. | | 422 | `validation_error` | A field is missing or invalid; `field` names it. | | 429 | `rate_limited`, `daily_quota_exceeded` | Too many requests, or the quota is used up. | --- # Cancel a scheduled email > Cancels a scheduled email that has not been sent yet. Source: https://docs.pidgely.com/api-reference/v1/emails/cancel/ `POST https://api.pidgely.com/v1/emails/{id}/cancel` Cancels a scheduled email that has not been sent yet. **Authorization:** `Bearer pdg_…`. An API key with the `full` scope. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | Resource id UUID. | ## Request ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/v1/emails/0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21/cancel' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "id": "0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21", "status": "canceled" } ``` ## Errors Native routes return `{"error": {"code", "message", "field", "request_id"}}`. See [Errors](https://docs.pidgely.com/errors/#native-api-errors). | Status | Code | When | |---|---|---| | 401 | `missing_api_key` | No `Authorization: Bearer` header. | | 403 | `invalid_api_key`, `insufficient_scope` | The key is invalid or lacks the scope. | | 404 | `not_found` | The resource does not exist in this account. | | 422 | `validation_error` | A field is missing or invalid; `field` names it. | | 429 | `rate_limited`, `daily_quota_exceeded` | Too many requests, or the quota is used up. | --- # Add a suppression > Suppresses an address. Source: https://docs.pidgely.com/api-reference/v1/suppressions/create/ `POST https://api.pidgely.com/v1/suppressions` Suppresses an address. **Authorization:** `Bearer pdg_…`. An API key with the `full` scope. ## Body parameters JSON object. | Name | Type | Required | Description | |---|---|---|---| | `address` | string | Yes | Email address. | ## Request ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/v1/suppressions' \ -H "Authorization: Bearer $PIDGELY_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "address": "bounced@example.com" }' ``` ## Response `201 Created` with a JSON object. ## Errors Native routes return `{"error": {"code", "message", "field", "request_id"}}`. See [Errors](https://docs.pidgely.com/errors/#native-api-errors). | Status | Code | When | |---|---|---| | 401 | `missing_api_key` | No `Authorization: Bearer` header. | | 403 | `invalid_api_key`, `insufficient_scope` | The key is invalid or lacks the scope. | | 404 | `not_found` | The resource does not exist in this account. | | 422 | `validation_error` | A field is missing or invalid; `field` names it. | | 429 | `rate_limited`, `daily_quota_exceeded` | Too many requests, or the quota is used up. | --- # List suppressions > Lists suppressions. Paginate with after. Source: https://docs.pidgely.com/api-reference/v1/suppressions/list/ `GET https://api.pidgely.com/v1/suppressions` Lists suppressions. Paginate with `after`. **Authorization:** `Bearer pdg_…`. An API key with the `full` scope. ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `after` | string | No | — | | `limit` | integer | No | — | ## Request ```bash tab="cURL" curl -X GET 'https://api.pidgely.com/v1/suppressions?limit=50' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` with a JSON object. ## Errors Native routes return `{"error": {"code", "message", "field", "request_id"}}`. See [Errors](https://docs.pidgely.com/errors/#native-api-errors). | Status | Code | When | |---|---|---| | 401 | `missing_api_key` | No `Authorization: Bearer` header. | | 403 | `invalid_api_key`, `insufficient_scope` | The key is invalid or lacks the scope. | | 404 | `not_found` | The resource does not exist in this account. | | 422 | `validation_error` | A field is missing or invalid; `field` names it. | | 429 | `rate_limited`, `daily_quota_exceeded` | Too many requests, or the quota is used up. | --- # Remove a suppression > Removes an address from the suppression list. Returns 204 No Content. Source: https://docs.pidgely.com/api-reference/v1/suppressions/delete/ `DELETE https://api.pidgely.com/v1/suppressions/{address}` Removes an address from the suppression list. Returns `204 No Content`. **Authorization:** `Bearer pdg_…`. An API key with the `full` scope. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `address` | string | Yes | E-mail address | ## Request ```bash tab="cURL" curl -X DELETE 'https://api.pidgely.com/v1/suppressions/bounced@example.com' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `204 No Content` with an empty body. ## Errors Native routes return `{"error": {"code", "message", "field", "request_id"}}`. See [Errors](https://docs.pidgely.com/errors/#native-api-errors). | Status | Code | When | |---|---|---| | 401 | `missing_api_key` | No `Authorization: Bearer` header. | | 403 | `invalid_api_key`, `insufficient_scope` | The key is invalid or lacks the scope. | | 404 | `not_found` | The resource does not exist in this account. | | 422 | `validation_error` | A field is missing or invalid; `field` names it. | | 429 | `rate_limited`, `daily_quota_exceeded` | Too many requests, or the quota is used up. | --- # Retrieve usage > Returns today's counters (UTC day), the daily quota and the account status. Source: https://docs.pidgely.com/api-reference/v1/usage/retrieve/ `GET https://api.pidgely.com/v1/usage` Returns today's counters (UTC day), the daily quota and the account status. **Authorization:** `Bearer pdg_…`. An API key with the `full` scope. ## Request ```bash tab="cURL" curl -X GET 'https://api.pidgely.com/v1/usage' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` ```json { "today": { "accepted": 1520, "delivered": 1498, "bounced": 6, "complained": 0 }, "daily_quota": 500, "status": "sandbox" } ``` ### Response fields | Name | Type | Required | Description | |---|---|---|---| | `today` | object | No | — | | `today.accepted` | integer | No | — | | `today.delivered` | integer | No | — | | `today.bounced` | integer | No | — | | `today.complained` | integer | No | — | | `daily_quota` | integer | No | — | | `status` | string | No | Current status. | ## Errors Native routes return `{"error": {"code", "message", "field", "request_id"}}`. See [Errors](https://docs.pidgely.com/errors/#native-api-errors). | Status | Code | When | |---|---|---| | 401 | `missing_api_key` | No `Authorization: Bearer` header. | | 403 | `invalid_api_key`, `insufficient_scope` | The key is invalid or lacks the scope. | | 404 | `not_found` | The resource does not exist in this account. | | 422 | `validation_error` | A field is missing or invalid; `field` names it. | | 429 | `rate_limited`, `daily_quota_exceeded` | Too many requests, or the quota is used up. | --- # Create a webhook (native event names) > Creates a webhook with native event names (email.queued, email.deferred, email.unsubscribed, email.canceled, domain.status_changed, account.status_changed, …). An empty events list subscribes to… Source: https://docs.pidgely.com/api-reference/v1/webhooks/create/ `POST https://api.pidgely.com/v1/webhooks` Creates a webhook with **native** event names (`email.queued`, `email.deferred`, `email.unsubscribed`, `email.canceled`, `domain.status_changed`, `account.status_changed`, …). An empty `events` list subscribes to every event. See [Webhooks](https://docs.pidgely.com/guides/webhooks/#native-events). **Authorization:** `Bearer pdg_…`. An API key with the `full` scope. ## Body parameters JSON object. | Name | Type | Required | Description | |---|---|---|---| | `url` | string | Yes | Public `https` URL that receives the events. | | `events` | string[] | No | Event types to subscribe to, for example `email.delivered`. See [Webhooks](https://docs.pidgely.com/guides/webhooks/#events). | ## Request ```bash tab="cURL" curl -X POST 'https://api.pidgely.com/v1/webhooks' \ -H "Authorization: Bearer $PIDGELY_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "url": "https://acme.com/webhooks/pidgely", "events": [ "email.delivered", "email.bounced" ] }' ``` ## Response `201 Created` with a JSON object. ## Errors Native routes return `{"error": {"code", "message", "field", "request_id"}}`. See [Errors](https://docs.pidgely.com/errors/#native-api-errors). | Status | Code | When | |---|---|---| | 401 | `missing_api_key` | No `Authorization: Bearer` header. | | 403 | `invalid_api_key`, `insufficient_scope` | The key is invalid or lacks the scope. | | 404 | `not_found` | The resource does not exist in this account. | | 422 | `validation_error` | A field is missing or invalid; `field` names it. | | 429 | `rate_limited`, `daily_quota_exceeded` | Too many requests, or the quota is used up. | --- # List webhooks > Lists native webhooks. Source: https://docs.pidgely.com/api-reference/v1/webhooks/list/ `GET https://api.pidgely.com/v1/webhooks` Lists native webhooks. **Authorization:** `Bearer pdg_…`. An API key with the `full` scope. ## Request ```bash tab="cURL" curl -X GET 'https://api.pidgely.com/v1/webhooks' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `200 OK` with a JSON object. ## Errors Native routes return `{"error": {"code", "message", "field", "request_id"}}`. See [Errors](https://docs.pidgely.com/errors/#native-api-errors). | Status | Code | When | |---|---|---| | 401 | `missing_api_key` | No `Authorization: Bearer` header. | | 403 | `invalid_api_key`, `insufficient_scope` | The key is invalid or lacks the scope. | | 404 | `not_found` | The resource does not exist in this account. | | 422 | `validation_error` | A field is missing or invalid; `field` names it. | | 429 | `rate_limited`, `daily_quota_exceeded` | Too many requests, or the quota is used up. | --- # Delete a webhook > Deletes a webhook. Returns 204 No Content. Source: https://docs.pidgely.com/api-reference/v1/webhooks/delete/ `DELETE https://api.pidgely.com/v1/webhooks/{id}` Deletes a webhook. Returns `204 No Content`. **Authorization:** `Bearer pdg_…`. An API key with the `full` scope. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | Resource id UUID. | ## Request ```bash tab="cURL" curl -X DELETE 'https://api.pidgely.com/v1/webhooks/0199d7f2-8b1d-7c3a-a2e5-4f9c6d0b1e73' \ -H "Authorization: Bearer $PIDGELY_API_KEY" ``` ## Response `204 No Content` with an empty body. ## Errors Native routes return `{"error": {"code", "message", "field", "request_id"}}`. See [Errors](https://docs.pidgely.com/errors/#native-api-errors). | Status | Code | When | |---|---|---| | 401 | `missing_api_key` | No `Authorization: Bearer` header. | | 403 | `invalid_api_key`, `insufficient_scope` | The key is invalid or lacks the scope. | | 404 | `not_found` | The resource does not exist in this account. | | 422 | `validation_error` | A field is missing or invalid; `field` names it. | | 429 | `rate_limited`, `daily_quota_exceeded` | Too many requests, or the quota is used up. | --- # Changelog > Notable changes to the Pidgely API, SMTP relay and documentation. Source: https://docs.pidgely.com/changelog/ Pidgely is in early access. Breaking changes to the API are avoided; when one is unavoidable it will be announced here first. ## October 2026: early access **API** - REST API at `https://api.pidgely.com`: emails (send, batch with `strict`/`permissive` validation, retrieve, list, reschedule, cancel, attachments), domains, API keys (with domain-restricted sending keys), webhooks (Standard Webhooks signatures with `webhook-*` and `svix-*` headers, secret rotation) and suppressions. - Natural-language scheduling (`in 1 hour`, `tomorrow at 9am`) and remote attachments with `path`. - `Idempotency-Key` on sends, replayed for 24 hours. - Contacts with typed properties, segments, legacy audiences and broadcasts with personalization and RFC 8058 one-click unsubscribe (`{{{PIDGELY_UNSUBSCRIBE_URL}}}`). - Templates with typed variables, drafts, published versions and aliases; `template` in sends. **SMTP** - Submission server at `smtp.pidgely.com` (587/2587 STARTTLS, 465/2465 TLS), user `pidgely`, password = API key; idempotency and tags through headers. **Deliverability** - Own MTA with separate transactional and broadcast IP pools, warm-up, per-destination throttling, VERP bounce processing, feedback loops, automatic suppression and the reputation guard. **Docs** - docs.pidgely.com with guides, an API reference generated from OpenAPI 3.1, Markdown copies of every page, `llms.txt` and `llms-full.txt`. ## Coming next - Official SDKs for Node.js, Python, PHP, Go and Ruby, and the MCP server (`@pidgely/mcp`): in [preview](https://docs.pidgely.com/sdks/). - Not available yet: open/click tracking, inbound email, automations. --- # Support > How to get help with Pidgely - email support, what to include in a request, and where to check service status. Source: https://docs.pidgely.com/support/ Write to **[support@pidgely.com](mailto:support@pidgely.com)**. A person reads every message. ## What to include To get an answer faster, include: - your account email or organization name (never your API key); - the **email id** (`0199d8a2-…`) or the **`X-Request-Id`** response header of the failing request; - the endpoint, the status code and the error `name`; - for delivery problems: the recipient domain, the time, and the `email.delivery_delayed` / `email.bounced` webhook payload if you have it; - for DNS problems: the domain name. ## Status Check [status.pidgely.com](https://status.pidgely.com) for incidents and maintenance before debugging an outage on your side. ## Other channels - **Security issues:** [security@pidgely.com](mailto:security@pidgely.com) ([Security](https://docs.pidgely.com/security/)). - **Sales and partnerships:** [contact@pidgely.com](mailto:contact@pidgely.com). - **Early access:** [request access](https://pidgely.com/#waitlist). --- # Security > How to report a vulnerability to Pidgely, and how the platform protects API keys, secrets, webhooks and your sending reputation. Source: https://docs.pidgely.com/security/ ## Reporting a vulnerability Email **[security@pidgely.com](mailto:security@pidgely.com)** with a description, the steps to reproduce and the impact you expect. Please: - give us a reasonable time to fix the issue before disclosing it; - don't access, modify or delete data that isn't yours, and stop as soon as you reach someone else's data; - don't degrade the service (no denial of service, no spam, no social engineering of our team). We acknowledge reports quickly, keep you informed, and credit researchers who want it. Our contact is also published in [`/.well-known/security.txt`](https://docs.pidgely.com/.well-known/security.txt). ## How your data is protected - **API keys** are shown once and stored only as HMAC digests; revoked keys stop working within about 30 seconds. - **Secrets at rest**: DKIM private keys and webhook signing secrets are encrypted with AES-256-GCM. - **Transport**: the API is HTTPS only; SMTP submission requires TLS 1.2 or newer; outgoing mail uses TLS whenever the receiving server supports it, or always with `tls: enforced`. - **Webhooks** are signed (HMAC-SHA256, Standard Webhooks) and only delivered to public `https` endpoints; private and internal addresses are refused. - **Remote attachments** are fetched through the same SSRF guard, without following redirects. - **Abuse controls**: sandboxed new accounts, quotas, executable attachments blocked, header injection rejected, automatic pause on abnormal bounce or complaint rates. ## Your side - Keep API keys on the server and in a secret manager; prefer [domain-restricted sending keys](https://docs.pidgely.com/authentication/#domain-restricted-keys). - Always [verify webhook signatures](https://docs.pidgely.com/guides/webhooks/#verify-signatures) and check timestamps. - Publish DMARC for your domains to stop others from spoofing them.