ContentsWebhooks

Notifications

Webhooks

NearlyIn sends your server a signed POST whenever something changes about an entry. Your system keeps up without polling the API.

Setting up

  1. In the dashboard, open “Webhooks” on your product's page.
  2. Pick the destination “Your own server”, enter a public https address, choose mode and events.
  3. Copy the secret. It is shown this one time only.
  4. Send a test event from the delivery log and check that your server answers with 2xx.

Events

entry.createdAn address signed up

Someone put their address on the list. It is pending until they confirm it by mail; unconfirmed entries are deleted after 14 days without a further event.

{
  "id": "evt_3f9a1c0b7d2e4a6f8b1c2d3e",
  "type": "entry.created",
  "createdAt": "2026-10-11T09:14:02.110Z",
  "mode": "live",
  "productId": "cmgk3wq1a0000abcd5678ijkl",
  "data": {
    "entry": {
      "id": "cmgk3x9f20001abcd1234efgh",
      "email": "ada@example.com",
      "state": "pending",
      "createdAt": "2026-10-11T09:12:45.120Z",
      "confirmedAt": null,
      "invitedAt": null,
      "redeemedAt": null,
      "referralCode": "k4m9q2xh7d",
      "referrals": 0
    }
  }
}
entry.confirmedAn address was confirmed

The person pressed the button behind the link in the confirmation mail. The entry is now on the waitlist and can be invited.

{
  "id": "evt_3f9a1c0b7d2e4a6f8b1c2d3e",
  "type": "entry.confirmed",
  "createdAt": "2026-10-11T09:14:02.110Z",
  "mode": "live",
  "productId": "cmgk3wq1a0000abcd5678ijkl",
  "data": {
    "entry": {
      "id": "cmgk3x9f20001abcd1234efgh",
      "email": "ada@example.com",
      "state": "confirmed",
      "createdAt": "2026-10-11T09:12:45.120Z",
      "confirmedAt": "2026-10-11T09:14:02.004Z",
      "invitedAt": null,
      "redeemedAt": null,
      "referralCode": "k4m9q2xh7d",
      "referrals": 0
    }
  }
}
entry.invitedAn entry was invited

The invitation mail was sent, from the dashboard or through the API. The event does not contain the personal code: that only ever travels in the mail.

{
  "id": "evt_3f9a1c0b7d2e4a6f8b1c2d3e",
  "type": "entry.invited",
  "createdAt": "2026-10-11T09:14:02.110Z",
  "mode": "live",
  "productId": "cmgk3wq1a0000abcd5678ijkl",
  "data": {
    "entry": {
      "id": "cmgk3x9f20001abcd1234efgh",
      "email": "ada@example.com",
      "state": "invited",
      "createdAt": "2026-10-11T09:12:45.120Z",
      "confirmedAt": "2026-10-11T09:14:02.004Z",
      "invitedAt": "2026-10-12T08:00:00.000Z",
      "redeemedAt": null,
      "referralCode": "k4m9q2xh7d",
      "referrals": 0
    }
  }
}
entry.redeemedAn invitation was redeemed

Your server redeemed the personal code of an invitation (POST /api/v1/invites/redeem): the person has come in. Sent once per entry.

{
  "id": "evt_5b2c9e1f0a3d4c7b8e6f1a2b",
  "type": "entry.redeemed",
  "createdAt": "2026-10-12T08:31:10.412Z",
  "mode": "live",
  "productId": "cmgk3wq1a0000abcd5678ijkl",
  "data": {
    "entry": {
      "id": "cmgk3x9f20001abcd1234efgh",
      "email": "ada@example.com",
      "state": "redeemed",
      "createdAt": "2026-10-11T09:12:45.120Z",
      "confirmedAt": "2026-10-11T09:14:02.004Z",
      "invitedAt": "2026-10-12T08:00:00.000Z",
      "redeemedAt": "2026-10-12T08:31:10.398Z",
      "referralCode": "k4m9q2xh7d",
      "referrals": 0
    }
  }
}
entry.unsubscribedA person removed themselves

The person used the unsubscribe link of a mail. The entry is deleted; data.entry shows it as it was. Remove the address on your side as well.

{
  "id": "evt_3f9a1c0b7d2e4a6f8b1c2d3e",
  "type": "entry.unsubscribed",
  "createdAt": "2026-10-11T09:14:02.110Z",
  "mode": "live",
  "productId": "cmgk3wq1a0000abcd5678ijkl",
  "data": {
    "entry": {
      "id": "cmgk3x9f20001abcd1234efgh",
      "email": "ada@example.com",
      "state": "invited",
      "createdAt": "2026-10-11T09:12:45.120Z",
      "confirmedAt": "2026-10-11T09:14:02.004Z",
      "invitedAt": "2026-10-12T08:00:00.000Z",
      "redeemedAt": null,
      "referralCode": "k4m9q2xh7d",
      "referrals": 0
    }
  }
}
entry.deletedAn entry was deleted by your team

Someone of your team deleted the entry in the dashboard, or your backend did with DELETE /api/v1/entries/{id}. data.entry shows it as it was.

{
  "id": "evt_3f9a1c0b7d2e4a6f8b1c2d3e",
  "type": "entry.deleted",
  "createdAt": "2026-10-11T09:14:02.110Z",
  "mode": "live",
  "productId": "cmgk3wq1a0000abcd5678ijkl",
  "data": {
    "entry": {
      "id": "cmgk3x9f20001abcd1234efgh",
      "email": "ada@example.com",
      "state": "invited",
      "createdAt": "2026-10-11T09:12:45.120Z",
      "confirmedAt": "2026-10-11T09:14:02.004Z",
      "invitedAt": "2026-10-12T08:00:00.000Z",
      "redeemedAt": null,
      "referralCode": "k4m9q2xh7d",
      "referrals": 0
    }
  }
}

Checking the signature

Every call carries the header NearlyIn-Signature: an HMAC-SHA256 over timestamp and body with the endpoint's secret. Check it before you act on anything. With the package that is one line:

webhook.ts

// Receiving NearlyIn's webhooks: any runtime with the Fetch API (Next.js route, Hono, Deno, Workers).
import { WebhookVerificationError, verifyWebhook } from "nearlyin/webhooks";

/** POST /webhooks/nearlyin */
export async function onNearlyInWebhook(request: Request): Promise<Response> {
  // The signature is over the body exactly as sent: read it as text, do not parse it first.
  const payload = await request.text();
  try {
    const event = await verifyWebhook(
      payload,
      request.headers.get("NearlyIn-Signature"),
      process.env.NEARLYIN_WEBHOOK_SECRET!,
    );
    // A delivery may arrive more than once. `event.id` is the same each time: use it to skip repeats.
    if (event.type === "entry.confirmed") {
      console.log(`${event.data.entry.email} is on the waitlist.`);
    }
    // Answer fast with 2xx and do slow work afterwards; anything else is retried for about ten hours.
    return new Response(null, { status: 204 });
  } catch (error) {
    if (error instanceof WebhookVerificationError) return new Response(error.reason, { status: 400 });
    throw error;
  }
}
NearlyIn-Signatureheader · string · required

t=<unix seconds>,v1=<hex>. The hex is HMAC-SHA256 over <t>.<body> with the endpoint's signing secret (whsec_…). Verify it with verifyWebhook from nearlyin/webhooks, or by hand: recompute the HMAC over the raw body, compare in constant time, and refuse timestamps older than five minutes.

NearlyIn-Eventheader · string · required

The event type, the same as type in the body.

NearlyIn-Deliveryheader · string · required

Id of this delivery. Stays the same when the delivery is retried.

Retries

Only a 2xx answer within 5 seconds counts. Otherwise NearlyIn tries again up to five times, after 1, 5, 30, 120 and 480 minutes. Every attempt carries the same event id and a fresh signature, so you recognise repeats.

The delivery log shows the last 100 deliveries per endpoint, for 30 days. We delete an event's body as soon as it has been delivered or given up.

Fields

Fields of an event

idstring · required

Id of the event. The same for every attempt to deliver it: use it to recognise repeats.

type"entry.created" | "entry.confirmed" | "entry.invited" | "entry.redeemed" | "entry.unsubscribed" | "entry.deleted" | "webhook.test" · required

webhook.test is only sent when you press “Send test event” in the dashboard. It goes to the endpoint whatever events it listens for, and its data.entry is made up.

createdAtstring · required

When it happened.

mode"live" | "test" · required

Live endpoints only get live events, test endpoints only test events.

productIdstring · required
dataobject · required

Fields of an entry

idstring · required

Stable id of the entry.

emailstring · required

Trimmed and lowercased.

state"pending" | "confirmed" | "invited" | "redeemed" · required

pending: signed up, address not confirmed yet. confirmed: on the waitlist. invited: the invitation mail was sent. redeemed: your server redeemed the personal code; the person has come in.

createdAtstring · required

When the address signed up (or last asked for a new confirmation mail).

confirmedAtstring | null · required

When the address was confirmed.

invitedAtstring | null · required

When the entry was invited for the first time. Sending the invitation again does not change it.

redeemedAtstring | null · required

When your server redeemed the personal code of the invitation: the person has come in.

referralCodestring | null · required

Code of this entry's referral link. Append ?ref=<code> to the address of a page with the form: sign-ups through it that confirm move this entry up the line.

referralsinteger · required

Sign-ups through this entry's referral link that confirmed.