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...

Drains

Send the data where you work

Forward operational logs and per-number Analytics to your own HTTPS endpoints. Signed, deduplicable and retried until accepted.

Drains per project
5
Ordered sampling rules
20
Delivery attempts within 24 h
6
Analytics windows
1 to 168 h

Sources

Logs and Analytics, never content.

Pick one source per drain. Neither carries message text, contacts or credentials.

Operational logs. API requests, number updates and webhook deliveries, with request and trace IDs to join your own traces.
Hourly Analytics. A per-number snapshot every hour over a 1-, 24- or 168-hour window. Needs Analytics turned on.
Ordered sampling. Up to 20 rules from 0 to 100%. The first match decides, and records from one request stay together.

Delivery

A delivery contract you can build on.

JSON or NDJSON over HTTPS POST, HMAC-signed, at least once, with your own headers if you want them.

Signed. HMAC-SHA256 over the timestamp and the original body. Replace, regenerate or clear the secret.
Retried, never doubled. Every retry keeps the delivery ID in Idempotency-Key. Delivery diagnostics stay for 7 days.
Status you can see. Pending and failed deliveries, the last HTTP status and the last error. Pause, resume or delete.

In your code

Your receiver, in a few lines.

Verify, deduplicate and acknowledge. Or read the same metrics from the API.

app/drain/route.ts
import { createHmac, timingSafeEqual } from "node:crypto";

const MAX_AGE_SECONDS = 300; // Retries are re-signed with a fresh timestamp.

export async function POST(request: Request) {
  const body = Buffer.from(await request.arrayBuffer());
  const timestamp = request.headers.get("x-polymorfa-timestamp") ?? "";
  const age = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (!/^\d+$/.test(timestamp) || age > MAX_AGE_SECONDS) {
    return new Response(null, { status: 401 });
  }
  const expected = "sha256=" + createHmac("sha256", drainSecret)
    .update(timestamp + "." + body)
    .digest("hex");
  const given = request.headers.get("x-polymorfa-signature") ?? "";
  if (given.length !== expected.length ||
      !timingSafeEqual(Buffer.from(given), Buffer.from(expected))) {
    return new Response(null, { status: 401 });
  }

  // Take the delivery ID from the signed body, not a header. NDJSON
  // carries it on every line; retries keep it, so store once.
  const ndjson = request.headers.get("content-type")?.startsWith("application/x-ndjson");
  const envelope = JSON.parse(ndjson ? body.toString().split("\n")[0] : body.toString());
  await warehouse.insertOnce(envelope.deliveryId, body);
  return new Response(null, { status: 204 });
}

API surface

The delivery contract.

Set drains up in the Console. Every delivery reaches your endpoint with these headers and one of these bodies.

Request

  • POSThttps://your-endpointOne HTTPS POST per delivery, 10 s timeout
  • HeaderX-Polymorfa-TimestampUnix seconds of this attempt
  • HeaderX-Polymorfa-Signaturesha256=HMAC(timestamp + "." + body)
  • HeaderIdempotency-KeyThe delivery ID, the same on every retry

Bodies

  • Bodyapplication/json{ version, deliveryId, records }
  • Bodyapplication/x-ndjsonOne line of { version, deliveryId, record }

Pull instead

  • GET/platform/analytics/metricsMetrics for your collector
  • GET/platform/projects/{projectId}/request-logsRequest logs, with logs:read

Start with a free number

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