# Webhooks

React to payment and subscription events the moment they happen. Rebill sends a JSON POST to your endpoint when state changes, so you can grant access, sync with your CRM, trigger emails, or update your database without polling.

## Available events

* `payment.created`: the payment was just submitted. Branch on `status` to tell approved from rejected.
* `payment.updated`: an existing payment's `status` changed after creation (for example a refund or a chargeback). Settlement and accreditation don't change `status`, so they don't fire it.
* `payment.available`: the net of an approved payment reached your available balance and can be withdrawn. See [Knowing when funds are available](#knowing-when-funds-are-available).
* `payout.updated`: a payout reached a terminal state — `completed` when the destination bank accepted the transfer, `failed` when it did not.
* `subscription.created`: fires right after the first approved payment of a new subscription.
* `subscription.updated`: any status transition on a subscription, **or** a replacement of the card that backs it, **or** a change of plan. `statusDetail` and `lastStatusDetail` tell you whether a pause was manual or caused by failed payments; `cardUpdated` and `planUpdated` tell you the delivery is a card swap or a plan change rather than a status change.
* `subscription.renewal_soon`: fires three days before the next scheduled charge, for both card and APM subscriptions.

## The envelope

Every delivery wraps event data inside a two-key object. The event type lives on `webhook.event`; the event-specific body lives on `data`.

```json
{
  "data": { /* event-specific shape, see reference below */ },
  "webhook": {
    "id": "we_f2b90261588e45edaf93c488e577ac66",
    "event": "payment.created",
    "url": "https://your-app.com/webhooks/rebill",
    "logId": "we_log_4a72d1b8e95a4e7a8d113f229bc4e682"
  }
}
```

Use `webhook.logId` as your deduplication key. It's unique per delivery attempt and stable across retries of the same event.

## Delivery contract

Your endpoint has **9 seconds** to respond with a `2xx`. Anything slower, any 5xx, any network failure, and Rebill enters the retry queue.

The default schedule is three retries:

| Attempt   | Delay after previous attempt |
| :-------- | :--------------------------- |
| 1st retry | 5 minutes                    |
| 2nd retry | 1 hour                       |
| 3rd retry | 1 day                        |

After the third failed retry, Rebill stops and flags the delivery as failed in the dashboard log. You can customize this schedule per organization via `PUT /v3/webhooks/retry-settings` — see the [Webhooks API reference](/api/reference/webhooks).

**Rebill signs every delivery.** Each POST carries an `x-rebill-signature` header holding an HMAC-SHA256 of the raw body, keyed with that webhook's signing secret. Verify it before you act on the event — see [Verify the signature](#verify-the-signature). As an extra layer, put an unguessable secret segment in your endpoint path (`/webhooks/rebill/9f2c...`) so unsigned traffic never reaches your handler at all.

## Register a webhook

From the dashboard: **Developer Center > Webhooks > Create Webhook**. Paste your endpoint URL, pick the events, save.

Programmatically: `POST /v3/webhooks` with your secret key. The full CRUD surface (create, update, delete, test, configure retries) is documented in the [Webhooks API reference](/api/reference/webhooks).

## Verify the signature

Every delivery carries an `x-rebill-signature` header. Its value is the HMAC-SHA256 of the **raw request body**, hex-encoded, keyed with that webhook's signing secret:

```
x-rebill-signature: 8f2b1d0c4a7e963f5b8c2d1a0e4f7b3c6d9a2e5f8b1c4d7a0e3f6b9c2d5a8e1f
```

That's the whole scheme. No timestamp, no version prefix, no comma-separated fields — the header is the hex digest and nothing else.

### Get the signing secret

Each webhook has its own secret, formatted as `whsec_` followed by 64 hex characters. Rebill shows it **once**, when the webhook is created: in the `signingSecret` field of the `POST /v3/webhooks` response, and on the creation screen in the Developer Center. No endpoint reads it back afterwards.

Use the secret exactly as returned, `whsec_` prefix included — that full string is the HMAC key.

If you don't have it — you never stored it, or the webhook predates signing and was assigned one by migration that was never shown — generate one from **Developer Center > Webhooks > ⋮**. The action reads **Generate signing secret** while no secret has ever been shown for that webhook, and **Regenerate secret** afterwards. Both are the same call from the API: `POST /v3/webhooks/:id/rotate`, whose response is the other place the secret appears.

Rotating takes effect immediately. The previous secret stops working as soon as the call returns, and deliveries already queued for retry are signed with the new one. Have the new value ready to deploy before you rotate, or your handler will reject valid deliveries until you do.

`GET /v3/webhooks/:id` used to return `signingSecret` and no longer does. If you wrote code against an earlier version of this page that fetched the secret at boot, replace it with the stored value.

### Sign the raw bytes, not the parsed body

Compute the HMAC over the exact bytes Rebill sent. Comparing against `JSON.stringify(req.body)` usually works and then breaks without warning: re-serializing a parsed object can reorder keys, change number formatting, or re-escape unicode, producing bytes that differ from what we signed. Keep the raw buffer instead — either `express.raw({ type: 'application/json' })`, or the `verify` option on `express.json()` as below.

Compare with `crypto.timingSafeEqual`, not `!==`. A plain string comparison returns early on the first differing byte, which leaks how much of a guessed signature was correct.

```javascript
const crypto = require('crypto')

// Stash the raw bytes on the request before Express parses them.
app.use(express.json({
  verify: (req, _res, buf) => { req.rawBody = buf },
}))

function isValidSignature(rawBody, header, secret) {
  if (typeof header !== 'string' || !rawBody) return false

  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest()
  const received = Buffer.from(header, 'hex')

  // Buffer.from silently drops invalid hex, so check the length first —
  // timingSafeEqual throws on mismatched buffer sizes.
  if (received.length !== expected.length) return false

  return crypto.timingSafeEqual(received, expected)
}

app.post('/webhooks/rebill', (req, res) => {
  const valid = isValidSignature(
    req.rawBody,
    req.get('x-rebill-signature'),
    process.env.REBILL_SIGNING_SECRET,
  )

  if (!valid) {
    return res.sendStatus(401)
  }

  res.sendStatus(200)
  handleEvent(req.body)
})
```

### What the signature does not prove

The scheme carries no timestamp, so a valid signature tells you the body came from Rebill — not that it arrived just now. Anyone who captures a delivery can replay those exact bytes later and the signature still verifies. Two habits close that gap, and both are worth keeping regardless:

* **Dedupe by `webhook.logId`.** It's unique per delivery attempt and stable across retries, so a replayed body is one you've already processed.
* **Confirm against the API before anything sensitive.** `GET /v3/payments/:id` with your `sk_` returns the authoritative state. Grant access and move money based on that, not on the webhook body.

## Test before going live

From the dashboard, open the **⋮** menu next to any webhook and select **Test**. Pick the event type, click **Send test event**, and Rebill fires a synthetic payload with the real payload structure to your URL.

From your code: `POST /v3/webhooks/:id/test` with `{ "event": "payment.created" }` in the body. The event must be one your webhook is subscribed to.

## Handler example

```javascript
app.post('/webhooks/rebill/:secret', (req, res) => {
  // Defense in depth — reject any POST that doesn't carry the secret path segment.
  if (req.params.secret !== process.env.REBILL_WEBHOOK_SECRET) {
    return res.sendStatus(404)
  }

  // isValidSignature is defined under "Verify the signature" above.
  const valid = isValidSignature(
    req.rawBody,
    req.get('x-rebill-signature'),
    process.env.REBILL_SIGNING_SECRET,
  )

  if (!valid) {
    return res.sendStatus(401)
  }

  const { data, webhook } = req.body

  // Respond fast, then process. Long work goes to a queue.
  res.sendStatus(200)

  switch (webhook.event) {
    case 'payment.created':
      if (data.payment.status === 'approved') {
        confirmPayment(data.payment.id)
      } else {
        handleFailedPayment(data.payment.id)
      }
      break

    case 'payment.updated':
      reconcilePayment(data.id, data.status)
      break

    case 'payment.available':
      markFundsAvailable(data.id, data.net.amount, data.net.currency)
      break

    case 'payout.updated':
      reconcilePayout(data.id, data.status)
      break

    case 'subscription.created':
      grantAccess(data.customer.id, data.subscription.id)
      break

    case 'subscription.updated':
      handleSubscriptionTransition(data)
      break

    case 'subscription.renewal_soon':
      notifyUpcomingCharge(data.customer.id, data.amount, data.currency)
      break
  }
})
```

Your handler must be **idempotent**. Rebill redelivers on retries, and at-least-once delivery means you will see the same event twice under bad network conditions. Dedupe by `webhook.logId` or by the underlying resource state.

## Reading subscription transitions

The `subscription.updated` event tells you **what** changed via `lastStatus` → `status`, and **why** via `lastStatusDetail` and `statusDetail`. A paused subscription due to defaulted payments looks different from one paused manually:

| Scenario                         | `lastStatus` | `status` | `lastStatusDetail` | `statusDetail` |
| :------------------------------- | :----------- | :------- | :----------------- | :------------- |
| Active → Paused (defaulted)      | `active`     | `paused` | `null`             | `defaulted`    |
| Active → Paused (manual)         | `active`     | `paused` | `null`             | `null`         |
| Paused (defaulted) → Reactivated | `paused`     | `active` | `defaulted`        | `null`         |

```javascript
function handleSubscriptionTransition(data) {
  if (data.status === 'cancelled') {
    revokeAccess(data.id)
    return
  }

  if (data.status === 'paused' && data.statusDetail === 'defaulted') {
    pauseAccess(data.id)
    notifyCustomerOfDefault(data.id)
    return
  }

  if (data.status === 'paused') {
    pauseAccess(data.id)  // manual pause, keep the customer informed normally
    return
  }

  if (data.status === 'active') {
    grantAccess(data.id)
  }
}
```

## Detecting a card change

`subscription.updated` also fires when the card backing a subscription is replaced — through the [hosted renewal page](/sdk/renew-subscription-card), the embedded renewal components, a renewal checkout paid with a different card, or a merchant-side update from the dashboard or the API.

On these deliveries the subscription's status did not change, so `status` and `lastStatus` carry the same value. **`cardUpdated` is the only reliable discriminator** — don't try to infer a card swap from the statuses. When it's `true`, a `card` object carries the card the subscription will charge from now on:

```json
{
  "id": "sub_1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d",
  "organization": { "id": "org_3f662e66b5f24b548f59331aa89b4fbf" },
  "status": "active",
  "lastStatus": "active",
  "statusDetail": null,
  "lastStatusDetail": null,
  "cardUpdated": true,
  "card": { "id": "crd_9e8d7c6b5a4938271605f4e3d2c1b0a9", "brand": "visa", "lastFourDigits": "4242" }
}
```

`cardUpdated` is present on every `subscription.updated` delivery, so `if (data.cardUpdated)` is always a safe check. Status transitions carry `cardUpdated: false` and no `card` object:

```javascript
function handleSubscriptionUpdated(data) {
  if (data.cardUpdated) {
    updateStoredCard(data.id, data.card)  // brand and lastFourDigits for your billing screen
    return
  }
  handleSubscriptionTransition(data)
}
```

Two details worth designing around:

* **A single request that changes both the status and the card produces two deliveries** — one for the transition, one for the card — never one combined event. Each is unambiguous on its own.
* **Re-authorizing the same card is not a card change.** Rebill only emits when the stored card actually changes, so a customer re-entering the card already on file produces nothing.

## Detecting a plan change

`subscription.updated` also fires when a subscription is moved to a different plan through [`PATCH /v3/subscriptions/:id/plan`](/api/reference/subscriptions#update-subscription-plan) or from the dashboard — upgrades and downgrades alike.

As with a card swap the status did not change, so `status` and `lastStatus` carry the same value and **`planUpdated` is the discriminator**. The `plan` object carries the plan the subscription now follows, including the amount and frequency it will be charged from now on, so you can tell an upgrade from a downgrade without re-reading the subscription:

```json
{
  "id": "sub_1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d",
  "organization": { "id": "org_3f662e66b5f24b548f59331aa89b4fbf" },
  "status": "active",
  "lastStatus": "active",
  "statusDetail": null,
  "lastStatusDetail": null,
  "cardUpdated": false,
  "planUpdated": true,
  "plan": {
    "id": "pln_647838573cf58d61bc659f6525f528ba",
    "previousId": "pln_0f3d9c2b8a14476e91c5d7e2f6b03a58",
    "amount": "29990",
    "currency": "ARS",
    "frequency": { "period": "month", "quantity": 1 }
  }
}
```

`planUpdated` is present on every `subscription.updated` delivery, so `if (data.planUpdated)` is always a safe check. Status transitions and card swaps carry `planUpdated: false` and no `plan` object:

```javascript
function handleSubscriptionUpdated(data) {
  if (data.cardUpdated) {
    updateStoredCard(data.id, data.card)
    return
  }

  if (data.planUpdated) {
    syncEntitlements(data.id, data.plan)  // plan.amount against the previous one tells you upgrade from downgrade
    return
  }

  handleSubscriptionTransition(data)
}
```

Three details worth designing around:

* **`previousId` is `null` when the subscription had no plan.** Subscriptions created straight from a price carry no plan until the first change, so treat `null` as "no previous plan" rather than as an error.
* **Re-sending the plan the subscription already has emits nothing.** Rebill only delivers when the plan actually changes, so an idempotent retry of your `PATCH` produces no webhook.
* **`amount` is the new recurring price**, in the same string format as `subscription.renewal_soon`. It is what the next charge will be, not a proration for the current period.

## Knowing when funds are available

`payment.created` tells you a payment was approved; the money is not in your available balance yet. It becomes available after the [accreditation period](/guides/balances#accreditation-period). `payment.available` fires when that happens, with the net that was added to your balance:

* **Local payments** (`processingMode: "local"`): when Rebill credits the payment's net to your balance, after the acquirer settled it and its accreditation date passed.
* **Cross-border payments** (`processingMode: "crossborder_per_transaction"`): on the payment's accreditation date. The net is in USD, the currency of the cross-border balance.

What to expect:

* **One delivery per credit.** A payment collected in installments (Brazil) is credited one installment at a time, so you get one `payment.available` per installment; `installment` tells you which one. Otherwise `installment` is `null`.
* **`net` is what you can withdraw** for that credit, after fees and taxes. `gross` is the amount before them, in the same currency, or `null` if it isn't known yet.
* **`availableAt`** is when the funds became available. For cross-border payments it is the start (00:00 UTC) of the accreditation date.
* **Delivery lag.** Rebill checks for newly available funds every 15 minutes, so the webhook can arrive up to about 15 minutes after `availableAt`.
* **It does not fire again** if the payment is later refunded or charged back; that arrives as `payment.updated`, and your balance reflects the debit.
* **Funds that became available before October 3, 2026** were not notified.

`payment.available` is not a status change: the payment keeps `status: "approved"`.

***

## Troubleshooting

When events don't arrive or your endpoint returns errors:

1. **Is the URL public?** Localhost and private IPs are unreachable. Use a tunnel (ngrok, Cloudflare Tunnel) during development.
2. **Is it HTTPS?** Rebill rejects non-TLS URLs.
3. **Check the delivery log.** The dashboard shows every attempt, the HTTP status your endpoint returned, and the payload sent. A `0` status code means a timeout or network failure on your side.
4. **Inspect the raw request.** Point the webhook at a request inspector (webhook.site, RequestBin) to confirm Rebill is sending what you expect. Swap back after debugging.

If events reach your endpoint but your logic breaks, log `webhook.logId` and `webhook.event` on every invocation — both values are in the dashboard's delivery log, so you can cross-reference from either side.

If your infrastructure requires an IP allowlist, Rebill sends webhooks from `52.158.209.200`. The legacy IPs `34.225.22.63` and `3.221.139.192` were retired on April 14, 2026 and are no longer in use.

## Payload reference

```json {{ title: 'payment.created' }}
{
  "data": {
    "payment": {
      "id": "pay_4f7b2c9da8e14c2e9f3a5d6c1e22bcd8",
      "createdAt": "2026-04-22T12:00:30.512Z",
      "amount": 15500,
      "currency": "ARS",
      "subTotalAmount": 15500,
      "quantity": 1,
      "country": "AR",
      "status": "approved",
      "name": "Premium Plan",
      "description": "Premium Plan",
      "origin": "checkout_landing",
      "oneClick": false,
      "paymentType": "first_subscription_payment",
      "paymentMethodType": "card",
      "metadata": { "paymentLinkId": "pl_6d1a4c7e9b3a4a8b8e21d5ef7ab3c091" }
    },
    "paymentLinkId": "pl_6d1a4c7e9b3a4a8b8e21d5ef7ab3c091",
    "planId": "pln_c8f2b7d94a1e4f17a6de9b2c5f0a3471",
    "subscriptionId": "sub_1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d",
    "organization": { "id": "org_3f662e66b5f24b548f59331aa89b4fbf", "name": "Example Organization" },
    "billingAddress": {
      "country": "AR",
      "state": "Buenos Aires",
      "zipCode": "1000",
      "addressLine1": "Av. Corrientes 1234"
    },
    "card": {
      "id": "crd_9e8d7c6b5a4f3e2d1c0b9a8e7d6c5b4a",
      "type": "credit",
      "brand": "visa",
      "bin": "453914",
      "lastFourDigits": "4242",
      "name": "Ana Lopez"
    },
    "customer": {
      "id": "cus_786176c9d39f40d8af98428edb2aa3db",
      "firstName": "Ana",
      "lastName": "Lopez",
      "email": "ana.lopez@example.com",
      "defaultLanguage": "es",
      "phone": "+541112345678"
    }
  },
  "webhook": {
    "id": "we_f2b90261588e45edaf93c488e577ac66",
    "event": "payment.created",
    "url": "https://example.com/webhooks/rebill",
    "logId": "we_log_4a72d1b8e95a4e7a8d113f229bc4e682"
  }
}
```

```json {{ title: 'payment.updated' }}
{
  "data": {
    "id": "pay_4f7b2c9da8e14c2e9f3a5d6c1e22bcd8",
    "organization": { "id": "org_3f662e66b5f24b548f59331aa89b4fbf" },
    "status": "refunded",
    "lastStatus": "approved",
    "metadata": { "paymentLinkId": "pl_6d1a4c7e9b3a4a8b8e21d5ef7ab3c091" }
  },
  "webhook": {
    "id": "we_f2b90261588e45edaf93c488e577ac66",
    "event": "payment.updated",
    "url": "https://example.com/webhooks/rebill",
    "logId": "we_log_7b31c8a21ef14a61b93f4c85d2e1bd17"
  }
}
```

```json {{ title: 'payment.available' }}
{
  "data": {
    "id": "pay_4f7b2c9da8e14c2e9f3a5d6c1e22bcd8",
    "organization": { "id": "org_3f662e66b5f24b548f59331aa89b4fbf" },
    "transactionId": "trx_a3c1e7f25b9d4e8a9c2f7d1b6e4a0c35",
    "processingMode": "local",
    "availableAt": "2026-10-05T12:30:00.000Z",
    "net": { "amount": 95.5, "currency": "BRL" },
    "gross": { "amount": 100, "currency": "BRL" },
    "installment": { "number": 2, "total": 3 }
  },
  "webhook": {
    "id": "we_f2b90261588e45edaf93c488e577ac66",
    "event": "payment.available",
    "url": "https://example.com/webhooks/rebill",
    "logId": "we_log_5d2a8c41f7e94b0e8a6f3c1d9b2e7a40"
  }
}
```

```json {{ title: 'payout.updated' }}
{
  "data": {
    "id": "pyt_9d2e5f7a3b8c4e1f6a2d5c7b9e0f1a2b",
    "organization": { "id": "org_3f662e66b5f24b548f59331aa89b4fbf" },
    "status": "completed",
    "lastStatus": "processing",
    "amount": 1500,
    "currency": "ARS",
    "recipientId": "rcp_7c9e6a40f2d34b1e8a5c3d7e9f0b2a4d",
    "bankAccountId": "rba_2e8f4a6c0d9b4f7e1a3c5d8b0e2f4a6c",
    "reference": "INV-0001",
    "failure": null,
    "metadata": { "trackingNumber": "TRK-8F3K2Q", "category": "payout" }
  },
  "webhook": {
    "id": "we_f2b90261588e45edaf93c488e577ac66",
    "event": "payout.updated",
    "url": "https://example.com/webhooks/rebill",
    "logId": "we_log_1c5e9b37d2f84a61b93f4c85d2e1bd19"
  }
}
```

```json {{ title: 'subscription.created' }}
{
  "data": {
    "subscription": {
      "id": "sub_1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d",
      "title": "Monthly Premium Plan",
      "description": "Premium subscription billed monthly",
      "planId": "pln_c8f2b7d94a1e4f17a6de9b2c5f0a3471",
      "currency": "ARS",
      "amount": 1990,
      "frequency": { "period": "month", "count": 1 },
      "defaultPaymentMethod": "card",
      "status": "active",
      "createdAt": "2026-04-22T12:00:00.000Z",
      "billingCycles": { "total": 12, "current": 1, "remaining": 11 },
      "setupFee": 0,
      "debitDay": 15,
      "nextChargeDate": "2026-05-22T12:00:00.000Z",
      "lastChargeDate": "2026-04-22T12:00:00.000Z",
      "metadata": { "customField": "value" }
    },
    "discount": {
      "couponCode": "SAVE10",
      "type": "percentage",
      "percentage": 10,
      "billingCyclesToApply": { "total": 3, "remaining": 2 }
    },
    "card": {
      "id": "crd_9e8d7c6b5a4f3e2d1c0b9a8e7d6c5b4a",
      "brand": "visa",
      "type": "credit",
      "lastFourDigits": "4242"
    },
    "organization": { "id": "org_3f662e66b5f24b548f59331aa89b4fbf", "name": "Example Organization" },
    "customer": {
      "id": "cus_786176c9d39f40d8af98428edb2aa3db",
      "email": "ana.lopez@example.com",
      "firstName": "Ana",
      "lastName": "Lopez",
      "defaultLanguage": "es",
      "phone": "+541112345678"
    }
  },
  "webhook": {
    "id": "we_f2b90261588e45edaf93c488e577ac66",
    "event": "subscription.created",
    "url": "https://example.com/webhooks/rebill",
    "logId": "we_log_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6"
  }
}
```

```json {{ title: 'subscription.updated' }}
{
  "data": {
    "id": "sub_1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d",
    "organization": { "id": "org_3f662e66b5f24b548f59331aa89b4fbf" },
    "status": "paused",
    "lastStatus": "active",
    "statusDetail": "defaulted",
    "lastStatusDetail": null,
    "cardUpdated": false,
    "planUpdated": false,
    "metadata": { "customField": "value" }
  },
  "webhook": {
    "id": "we_f2b90261588e45edaf93c488e577ac66",
    "event": "subscription.updated",
    "url": "https://example.com/webhooks/rebill",
    "logId": "we_log_b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7"
  }
}
```

```json {{ title: 'subscription.renewal_soon' }}
{
  "data": {
    "id": "sub_1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d",
    "organization": { "id": "org_3f662e66b5f24b548f59331aa89b4fbf" },
    "amount": "1990",
    "currency": "ARS",
    "nextChargeDate": "2026-05-22T12:00:00.000Z",
    "defaultPaymentMethod": "card",
    "title": "Monthly Premium Plan",
    "description": "Premium subscription billed monthly",
    "card": {
      "id": "crd_9e8d7c6b5a4f3e2d1c0b9a8e7d6c5b4a",
      "brand": "visa",
      "type": "credit",
      "lastFourDigits": "4242"
    },
    "customer": {
      "id": "cus_786176c9d39f40d8af98428edb2aa3db",
      "email": "ana.lopez@example.com",
      "firstName": "Ana",
      "lastName": "Lopez",
      "defaultLanguage": "es",
      "phone": "+541112345678"
    },
    "planId": "pln_c8f2b7d94a1e4f17a6de9b2c5f0a3471",
    "metadata": { "customField": "value" }
  },
  "webhook": {
    "id": "we_f2b90261588e45edaf93c488e577ac66",
    "event": "subscription.renewal_soon",
    "url": "https://example.com/webhooks/rebill",
    "logId": "we_log_c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8"
  }
}
```

Note that `amount` on `subscription.renewal_soon` is a **string**. Every other event uses a number.
