> For the complete documentation index, see [llms.txt](https://docs.neobanc.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.neobanc.com/api-reference/webhooks.md).

# Webhooks

Neobanc sends webhook notifications to the URL you configure in your [API Partner Dashboard](https://app.neobanc.com/api-dashboard/webhooks). These are requests **Neobanc makes to you** — not endpoints you call.

## Events

| Event               | Sent when                                                                                                                                   |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `payment.created`   | A payment record is created via `POST /create-payment`                                                                                      |
| `payment.completed` | A payment transitions to completed — e.g. the Interac e-transfer is matched to incoming funds, or a credit card payment finishes processing |
| `payment.failed`    | A payment transitions to failed — e.g. the biller rejected it (commonly an invalid account number) or its settlement batch was rejected     |

Each event must be subscribed to explicitly in your webhook settings. A failed payment is not necessarily terminal: once the underlying issue is corrected, the payment is re-queued and a `payment.completed` event is sent if it later succeeds.

## Delivery policy

Delivery is best-effort and attempted once. Any 2xx response from your endpoint is treated as successful delivery; non-2xx responses are logged but **not retried**. Use `POST /payment-history` to reconcile if you miss an event.

## Payload

All events share the same shape — an `event` name and an array of payment records:

```json
{
  "event": "payment.completed",
  "payments": [
    {
      "id": 12345,
      "type": "bill",
      "userId": "user_abc",
      "amount": 125.5,
      "dueDate": "2026-04-15T00:00:00.000Z",
      "status": "completed",
      "identificationNumber": "123456789012",
      "source": "public-api",
      "createdAt": "2026-04-01T12:00:00.000Z",
      "paymentMade": "2026-04-02T09:30:00.000Z",
      "payeeName": "ENMAX ENERGY CORPORATION",
      "billType": "Electricity",
      "accountNumber": "1234567890",
      "paymentMethod": "interac",
      "paymentAmount": 125.5
    }
  ],
  "timestamp": "2026-04-02T09:30:01.000Z",
  "source": "neobanc"
}
```

Rent payments additionally carry `term`, `rent`, and `landlordMemo`; bill payments carry `payeeName`, `billType`, `accountNumber`, `recipientEmail`, `paymentMethod`, and `paymentAmount`. The full schemas are in the [OpenAPI specification](https://api.neobanc.com/openapi.json) under the top-level `webhooks` key.

## Verifying signatures

Every request includes a `Neobanc-Signature` header signed with the signing secret shown once in your webhook settings (format `whsec_...`) — distinct from your API token.

**Header format:**

```
Neobanc-Signature: t=<unix_timestamp>,v1=<hex_hmac_sha256>
```

**Signed string:** `${timestamp}.${rawRequestBody}` — use the raw, unparsed request body bytes exactly as received. Re-serializing the parsed JSON will produce a different string and cause valid signatures to fail.

To verify: recompute the HMAC-SHA256 of the signed string with your signing secret, compare it to `v1` with a constant-time comparison, and reject the request if `t` is more than 5 minutes old.

```javascript
const crypto = require("crypto");

function verifyNeobancSignature(rawBody, signatureHeader, signingSecret) {
  const parts = Object.fromEntries(
    signatureHeader.split(",").map((kv) => kv.split("="))
  );
  const { t: timestamp, v1: signature } = parts;

  // Reject stale timestamps (older than 5 minutes)
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

  const expected = crypto
    .createHmac("sha256", signingSecret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(signature, "hex"),
    Buffer.from(expected, "hex")
  );
}
```

```python
import hashlib, hmac, time

def verify_neobanc_signature(raw_body: bytes, signature_header: str, signing_secret: str) -> bool:
    parts = dict(kv.split("=") for kv in signature_header.split(","))
    timestamp, signature = parts["t"], parts["v1"]

    # Reject stale timestamps (older than 5 minutes)
    if abs(time.time() - int(timestamp)) > 300:
        return False

    signed = f"{timestamp}.".encode() + raw_body
    expected = hmac.new(signing_secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(signature, expected)
```

## Testing

Send mock payment data to your configured endpoint with `POST /webhook-test` (authenticated with your API token), or use the test button in the dashboard's webhook settings.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.neobanc.com/api-reference/webhooks.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
