Builders Program

Build with us for a whole month. Unlimited usage and add-ons, weekly calls and your own Slack channel.

Learn more
Polymorfa

Command Palette

Search for a command to run...

End-to-end encryption

Decide who can read your messages

Pick a data level for each number. Polymorfa stores chats regionally, stores nothing, or relays only ciphertext while the Signal keys stay on your servers or in the browser.

Data levels for each number
4
Signal keys we hold on zero-knowledge numbers
0
Longest queue copy without hosted storage
24 h
Databases with official adapters
9

Data levels

One number, four ways to hold its data

On hosted and pass-through numbers, Polymorfa handles message content to deliver it. On the two zero-knowledge levels, only you can decrypt it.

Hosted. Hosted Message Storage keeps chats, contacts and media in the number's region. Work from the Chats and Contacts inbox or the history API; pins, archives and read state sync with the phone.
Pass-through. The default. Each message is decrypted in memory and delivered to your webhook, and no content is written to a database. Event history reports the body as not retained.
Your servers. Run the Signal service on your infrastructure. It holds the Signal keys, decrypts each message and hands the plaintext to your app. Polymorfa relays ciphertext.
Browser. signal-wasm decrypts in your users' browsers or at the edge. Its key is a non-extractable WebCrypto key, so messages decrypt only on the client.

Zero-knowledge numbers

We carry the traffic, you hold the keys

Polymorfa keeps the WhatsApp connection, receipts, retries and pacing. Every operation that needs a private key goes to your Signal service, and message plaintext never reaches our servers, in memory or on disk.

Ciphertext in, ciphertext out. Your Signal service opens the connection to Polymorfa, so you expose no inbound port. One connection serves many numbers.
Open source. signal-node is published under AGPL-3.0 and built on whatsapp-rust, so you can read exactly what runs beside your keys.
Leases and failover. Each replica claims a number at a lease generation, and Polymorfa routes to the newest claim. Run two replicas and the standby takes over.
The trade-offs. Features that read message content cannot run without it, and requests for them return a clear error. Zero-knowledge numbers use Linked Devices and cost the same as standard ones.

Your database

Sync into the database you already run

Every SDK language shares one storage contract. Point a webhook handler or an event stream at an adapter, and chats, messages, contacts, media and history land in your own tables.

Nine databases, one contract. Upsert, patch, delete, deduplicate by event ID and resume from a saved cursor. Postgres, MySQL, SQLite, MongoDB and DynamoDB adapters exist in all six SDK languages.
A Supabase preset. Apply the schema with row-level security policies included, then query messages from your app like any other table.
ORM schema exports. Generate the tables as Drizzle, Prisma, SQLAlchemy, Django, Eloquent or EF Core models, and keep them in your own migrations.

Direct media and history

Media and history without a storage bill

The SDK uploads media to WhatsApp and downloads history chunks itself, so neither adds storage cost. Both are free, with no meter.

Direct upload. Encrypt and upload from your server, through the number's connection or from your own network. Send the result as a media descriptor.
Your bucket. Upload to WhatsApp from an S3-compatible object, and archive inbound media into your bucket before the CDN copy expires.
Direct history. Polymorfa forwards the chunk reference, and your SDK downloads and decodes it. Without the person's consent, nothing is forwarded.
  • On zero-knowledge numbers, the media key and plaintext never reach Polymorfa
  • Your Signal service handles history for zero-knowledge numbers entirely
  • Plaintext retry and event buffers are off for every number, enforced by the database

In your code

Keep your copy in a few lines

Store events in your database, upload media straight to WhatsApp, archive it to your bucket or read hosted chats.

app/webhooks/route.ts
import { webhooks } from "@polymorfa/sdk";
import { storageWebhookHandler } from "@polymorfa/storage";
import { postgresAdapter } from "@polymorfa/storage/postgres";

const store = storageWebhookHandler(
  postgresAdapter({ connectionString: process.env.DATABASE_URL! }),
);

export async function POST(request: Request) {
  const event = await webhooks.verify({
    body: await request.arrayBuffer(),
    signature: request.headers.get("x-webhook-signature") ?? "",
    secret: process.env.WEBHOOK_SECRET!,
  });
  await store(event); // chats, messages, contacts, media
  return Response.json({ received: event.id });
}

API surface

The data API

Hosted chats through the Messaging API, and the commands the SDK uses for direct media and history.

Hosted Message Storage

  • GET/messaging/{number}/chatsList stored chats
  • GET/messaging/{number}/chats/{chat}Get one chat
  • GET/messaging/{number}/chats/{chat}/messagesPage through stored messages
  • GET/messaging/{number}/chats/{chat}/messages/{id}/mediaDownload stored media

Direct media and history

  • POST/messaging/{number}/media/relay-uploadsUpload ciphertext through the number
  • POST/messaging/{number}/media/upload-grantsShort-lived grant to upload from your network
  • POST/messaging/{number}/history/chunk-retriesAsk the phone to send a chunk again
  • Eventhistory.syncA history chunk is ready to download

Start with a free number

Your first real number is free. Add more numbers on Pay-As-You-Go.