WEBHOOKS / GETTING STARTED

News that comes to you.

Save a semantic query and collect newly indexed matches in your dashboard inbox, with optional delivery to an HTTPS endpoint. Choose one-minute batches or hourly digests.

Use the monitoring inbox

Save a query in Dashboard → Monitoring and select “Inbox only” to start without a webhook. Browse matches in Dashboard → Inbox, filter by query or unread state, and mark articles read. Matches stay available for 30 days, including older query versions after edits or pauses. Adding a webhook sends the same accepted matches without charging twice. Inbox reads use no additional quota.

Inbox matching continues while a webhook is disabled or awaiting verification, provided the query is active. Those matches are not backfilled to the webhook when it becomes available. Queries paused by quota or backlog limits still require manual resume.

Saved queries and previews also accept requiredTerms and excludedPhrases: up to 10 case-insensitive literal substrings of 100 characters each. Every required entry must appear in the title or description; any excluded entry rejects a match before quota charging. Monitoring uses its minimum cosine similarity, rather than relative hybrid ranking.

Set up a receiver

  1. Open Dashboard → Monitoring and add a public HTTPS URL on port 443. Private networks, redirects and URLs with credentials are blocked.
  2. Copy the signing secret shown once and install it in your receiver as NEARWIRE_WEBHOOK_SECRET.
  3. Verify the endpoint. We POST a signed webhook.endpoint_verification event. Return HTTP 2xx with JSON {"challenge":"the received data.challenge"}.
  4. Save a query, choose sources and a similarity threshold, and preview current results to tune the threshold. Each preview consumes one search request; existing preview results are not delivered.

This Node.js example listens locally on port 3001. Run it behind your own HTTPS reverse proxy. The demonstration deduplication Set is lost on restart; use durable storage with a unique event ID and atomically persist processing work before returning 2xx in production.

receiver.mjs · NEARWIRE_WEBHOOK_SECRET=whsec_… node receiver.mjs
// Node.js example. Place behind a public HTTPS reverse proxy on port 443.
// Replace the demonstration Set with durable database deduplication in production.
import { createHmac, timingSafeEqual } from "node:crypto";
import { createServer } from "node:http";
import { pathToFileURL } from "node:url";

export function verifyWebhook(body, headers, secret, now = Date.now()) {
  const id = headers["webhook-id"], timestamp = headers["webhook-timestamp"];
  const signatures = headers["webhook-signature"];
  if (typeof id !== "string" || typeof timestamp !== "string" || typeof signatures !== "string" || !/^\d+$/.test(timestamp)) throw new Error("Missing signature");
  if (Math.abs(now / 1000 - Number(timestamp)) > 300) throw new Error("Stale signature");
  if (!secret?.startsWith("whsec_")) throw new Error("Missing signing secret");
  const expected = createHmac("sha256", Buffer.from(secret.slice(6), "base64")).update(`${id}.${timestamp}.${body}`).digest();
  const valid = signatures.split(" ").some(value => {
    const [version, signature] = value.split(",");
    if (version !== "v1" || !signature) return false;
    const received = Buffer.from(signature, "base64");
    return received.length === expected.length && timingSafeEqual(received, expected);
  });
  if (!valid) throw new Error("Invalid signature");
  const event = JSON.parse(body);
  if (event.id !== id) throw new Error("Event ID mismatch");
  return event;
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
  const secret = process.env.NEARWIRE_WEBHOOK_SECRET;
  if (!secret?.startsWith("whsec_")) throw new Error("Set NEARWIRE_WEBHOOK_SECRET");
  const seen = new Set(); // DEMO ONLY: use a UNIQUE event_id in durable storage.
  createServer(async (req, res) => {
    if (req.method !== "POST" || req.url !== "/webhooks/nearwire") { res.writeHead(404).end(); return; }
    try {
      let size = 0; const chunks = [];
      for await (const chunk of req) {
        size += chunk.length;
        if (size > 65_536) { res.writeHead(413).end(); return; }
        chunks.push(chunk);
      }
      const event = verifyWebhook(Buffer.concat(chunks).toString("utf8"), req.headers, secret);
      if (event.type === "webhook.endpoint_verification") {
        res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ challenge: event.data.challenge }));
        return;
      }
      if (event.type !== "query.matches") { res.writeHead(400).end(); return; }
      if (!seen.has(event.id)) {
        // Production: insert event + work into your DB/queue in one transaction,
        // with a UNIQUE event_id. Return 2xx only after that commit succeeds.
        console.log("New matches:", event.data.query.name, event.data.articles.map(a => a.title));
        seen.add(event.id);
      }
      res.writeHead(204).end();
    } catch {
      res.writeHead(400).end("Invalid webhook");
    }
  }).listen(Number(process.env.PORT ?? 3001), "127.0.0.1");
}

Verify every request

Use the exact raw UTF-8 body before parsing JSON. Nearwire follows the Standard Webhooks signing convention: webhook-id, webhook-timestamp and webhook-signature. Decode the base64 part of your whsec_ secret and compute HMAC-SHA256 over id.timestamp.rawBody. Compare against v1,base64Signature in constant time and reject timestamps outside five minutes.

Deduplicate by webhook-id. Retries and replay preserve the event ID and payload, but receive a fresh signature timestamp. Delivery is at least once, and events can arrive out of order. Respond within 10 seconds; process articles asynchronously after storing the event safely.

query.matches · example payload
{
  "id": "event-uuid",
  "type": "query.matches",
  "timestamp": "2026-10-06T15:00:00.000Z",
  "data": {
    "query": {
      "id": "query-uuid",
      "version": 1,
      "name": "Battery technology",
      "text": "Advances in grid-scale battery storage"
    },
    "articles": [
      {
        "id": "article-uuid",
        "title": "A new battery breakthrough",
        "description": "Article excerpt",
        "url": "https://example.com/news/battery",
        "author": null,
        "publishedAt": "2026-10-06T14:00:00.000Z",
        "feed": {
          "id": "feed-uuid",
          "title": "Example News"
        },
        "score": 0.72
      }
    ]
  }
}

Timing, limits and recovery

  • Every plan includes 5 active queries, 3 endpoints and 10,000 matched articles per UTC month. Your signup month is prorated by time remaining. Monitoring has a separate budget from search.
  • One matching article consumes one match for each query it satisfies. Delivery attempts, verification and replay consume no matches.
  • Monitoring starts from activation, resume or edit. An old publication date can still match if the article is newly indexed. There is no historical backfill; matches depend on source fetching and embedding completion.
  • Batch intervals start with the first pending match. Events contain at most 20 articles and 64 KiB; larger digests are split. Workers check for work each minute, so intervals are targets rather than delivery deadlines.
  • HTTP 2xx acknowledges delivery. Network failures, 408, 429 and 5xx retry with exponential backoff and jitter for up to 24 hours. Retry-After is honored up to one hour; other HTTP failures are final. Redirects are not followed.
  • After ten consecutive delivery failures, the endpoint is disabled. Fix the receiver and verify again. Queued, unexpired events can then continue; articles indexed while it was disabled remain eligible for the inbox but are not backfilled to the webhook.
  • Queries pause at the monthly match limit or 500 pending matches. Already accepted matches drain. Resume manually after quota resets or backlog clears.
  • Pause cancels queued events and discards unbatched matches. Saving an edit creates a new query version. A request already in flight may still complete. Secret rotation stops delivery until you install and verify the new secret.
  • Inspect events and attempts in Monitoring. Delivered or failed events can be replayed within the 30-day history window if their query version and endpoint are still active. Deleting a query or endpoint removes associated history.

Manage monitoring through the API

Use a key with monitoring:write for changes and monitoring:read for summaries, inbox and delivery history. Newly created keys default to search:read, so explicitly select monitoring permissions in Dashboard → API keys. Management calls share your account’s per-minute rate limit and do not consume search requests. Endpoint verification is limited to 10 attempts per 15 minutes; replay is limited to 30 requests per minute. Outbound delivery is capped at 60 requests per endpoint per minute.

1. Create endpoint · save endpoint.id and secret
curl "https://nearwire.dev/api/v1/webhook-endpoints" \
  -H "Authorization: Bearer $NEARWIRE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Production","url":"https://example.com/webhooks/nearwire"}'
2. Verify ownership after installing the secret
curl -X POST "https://nearwire.dev/api/v1/webhook-endpoints/$ENDPOINT_ID/verify" \
  -H "Authorization: Bearer $NEARWIRE_API_KEY"
3. Save query · intervalMinutes can be 1 or 60
curl "https://nearwire.dev/api/v1/saved-queries" \
  -H "Authorization: Bearer $NEARWIRE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Battery technology","query":"Advances in grid-scale battery storage","endpointId":"YOUR_ENDPOINT_UUID","minScore":0.35,"intervalMinutes":1,"feedIds":[]}'

GET /monitoring/inbox returns up to 50 matches with nextCursor, optional queryId and unreadOnly=true filters. PATCH /monitoring/inbox/{id} accepts a JSON read boolean. Omit endpointId or set it to null when saving an inbox-only query. GET /monitoring returns your queries, endpoints and quota. GET /webhook-events lists deliveries; GET /webhook-events/{id} includes payload and attempts. POST /webhook-events/{id}/replay requeues an event. See the API reference for editing, pause/resume, rotation and deletion.