npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pidgeon-ai/sdk

v1.67.4

Published

Email inboxes for AI agents — TypeScript SDK for Pidgeon: create addresses, send, receive and reply.

Readme

@pidgeon-ai/sdk

Pidgeon is the email inbox API for AI agents. It gives agents their own inboxes so they can work with emails just like a human. This is its TypeScript client: one typed function per operation, plus a webhook verifier.

npm install @pidgeon-ai/sdk

Or pnpm add @pidgeon-ai/sdk. Zero dependencies. Needs fetch and crypto.subtle, so it runs on Node 18+, Bun, Deno, Cloudflare Workers and Vercel functions. ESM and CommonJS.

Create an address, send, receive, reply

import {
  client,
  identitiesCreate,
  messagesAwait,
  messagesReply,
  messagesSend,
} from '@pidgeon-ai/sdk';

client.setConfig({ auth: () => process.env.PIDGEON_API_KEY! });

// 1. An address of its own. Any free name on pidgeon.email works with no DNS.
const { data: address } = await identitiesCreate({
  body: { address: '[email protected]', type: 'agent' },
  throwOnError: true,
});

// 2. Send from it.
await messagesSend({
  body: {
    from: address.data.email,
    to: '[email protected]',
    subject: 'Staging access',
    text: 'Could you send me the access code?',
  },
  throwOnError: true,
});

// 3. Receive: one held-open request until the answer arrives, up to 60 seconds.
const { data: awaited } = await messagesAwait({
  query: { identity_id: address.data.id, timeout: 60 },
  throwOnError: true,
});

// 4. Reply into the same conversation. Recipients and subject come off the original.
if (awaited.data.message) {
  console.log(awaited.data.message.extracted.codes);
  await messagesReply({
    path: { id: awaited.data.message.id },
    body: { text: 'Thanks, that worked.' },
    throwOnError: true,
  });
}

PIDGEON_API_KEY is a pgn_live_… key. Make one at pidgeon.ai/app/developers/keys, or let your agent sign itself up: pidgeon.ai/skill.md is two calls, and the only thing it needs from a person is a six-digit code.

In production, take a signed webhook rather than holding a request open. Documentation: pidgeon.ai/docs · API reference · OpenAPI.

The shape

There is no Pidgeon class. Every operation is a function named for its tag — messagesSend, messagesGet, threadsUpdate — and they share one client you configure at startup. The Python package is generated from the same document, so an operation has the same name in both.

import { client, messagesGet } from '@pidgeon-ai/sdk';

client.setConfig({ auth: () => process.env.PIDGEON_API_KEY! });

const { data, error } = await messagesGet({ path: { id: messageId } });

A call resolves to { data, error } rather than throwing: error carries the API's own code, a message written for a person, and a field when one input was at fault. Pass throwOnError: true on a call to get the data directly and an exception instead.

Receiving mail

Mail arriving at an address your code owns becomes an event, and your endpoint is called with a signed payload.

import { client, messagesGet, messagesReply, verifyWebhook } from '@pidgeon-ai/sdk';

client.setConfig({ auth: () => process.env.PIDGEON_API_KEY! });

export async function POST(request: Request) {
  // Verify first. An unverified payload is an unauthenticated POST that
  // claims a message arrived.
  const event = await verifyWebhook({
    rawBody: await request.text(),
    headers: request.headers,
    secret: process.env.PIDGEON_WEBHOOK_SECRET!,
  });

  if (event.type !== 'message.received') return new Response(null, { status: 204 });

  // The event carries ids, never a body. Exchange one for the content.
  const { data: mail } = await messagesGet({
    path: { id: event.data.message_id as string },
    throwOnError: true,
  });

  // Into the same conversation, threaded in the recipient's client too.
  await messagesReply({
    path: { id: mail.data.id },
    body: { text: 'Got it — someone will be in touch.' },
    throwOnError: true,
  });

  return new Response(null, { status: 204 });
}

rawBody must be the exact bytes we sent. Parse the JSON after verifying — re-serialising changes key order and the signature will not match.

verifyWebhook throws rather than returning a boolean, because the failure mode of a boolean is a caller who forgets to check it. It rejects a timestamp outside tolerance (five minutes by default) before it looks at the signature, and compares in constant time.

Handing a conversation to a person

import { threadsUpdate } from '@pidgeon-ai/sdk';

await threadsUpdate({
  path: { id: threadId },
  body: { owner_type: 'human' },
  throwOnError: true,
});

Hand it back by setting owner_type to agent.

It appears in the web inbox, and thread.updated tells whatever was driving the conversation that it no longer is.

Errors

A failure comes back in error, carrying the API's own code, a message written for a person and safe to show one, and the offending field on a validation failure.

const { data, error } = await messagesSend({ body: { from, to, text } });

if (error) {
  if (error.error.code === 'rate_limited') {
    // Back off. Deliberately not done for you — see below.
  }
  throw new Error(error.error.message);
}

code is one of validation, unauthenticated, entitlement, forbidden, not_found, conflict, rate_limited, internal.

With throwOnError: true the same failure is thrown instead and data is no longer undefined, which is the shape most call sites want.

Pagination

List endpoints return a page, not an iterator.

import { eventsList } from '@pidgeon-ai/sdk';

let cursor: string | undefined;

do {
  const { data: page } = await eventsList({
    query: { limit: 50, cursor },
    throwOnError: true,
  });

  for (const event of page.data) handle(event);
  cursor = page.next_cursor ?? undefined;
} while (cursor);

What this deliberately does not do

No retry policy, no queue, no caching, no auto-paginating iterator. Those are decisions that belong to your application — an SDK that makes them for you is one you eventually work around, and an iterator that hides pagination also hides the rate limit pagination is subject to.

Options

import { client } from '@pidgeon-ai/sdk';

client.setConfig({
  auth: () => process.env.PIDGEON_API_KEY!,
  // Point at a local dev server, or a preview deployment.
  baseUrl: 'http://localhost:3000/api/v1',
  // Inject a transport. Useful in tests.
  fetch: myFetch,
});

createClient(createConfig({ … })) makes a second, independent client, for code that talks to two accounts; pass it as client on a call.

Versioning

Versioned independently of the API. The API is /v1; this package is 1.x, and semver applies to its own surface — the exported types, the method signatures, the error shape. See CHANGELOG.md.

Contributing

This package is generated from openapi.json, the contract the API validates against, and published from pidgeon-ai/pidgeon-js. A pull request against generated code cannot be merged as written — the fix belongs in the schema. Open an issue and say what is wrong, or email [email protected].

MIT licensed.