> ## Documentation Index
> Fetch the complete documentation index at: https://apidocs.finrock.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Receive Real-Time Transaction Webhooks

> Receive Finrock transaction events via webhooks. Learn endpoint registration, JSON payload fields, retry schedule, and RSA-SHA512 signature verification.

Webhooks let your backend application react to deposit and withdrawal events the moment they happen, without polling the Finrock API. When you register a webhook URL, Finrock sends an HTTP POST request to that URL every time a transaction event occurs in your workspace — including new deposits, withdrawal confirmations, and status changes.

## Set Up Your Webhook

<Steps>
  ### Register Your Endpoint URL

  Open your browser and navigate to `https://finrock.io/webhooks`. Type your server's publicly accessible URL (for example, `https://yourbackend.com/webhook`) into the input field and click the **+** button to add it.

  You can register multiple webhook URLs if you need to fan events out to several services (for example, a notification service and an accounting system).

  ### Deploy Your Webhook Handler

  Your endpoint must accept `POST` requests and return an HTTP `200` status code to acknowledge receipt. Any non-200 response — or no response at all — tells Finrock the delivery failed and triggers the retry policy.

  Here is a minimal Node.js/Express handler that acknowledges events and verifies the signature:

  ```javascript theme={null}
  const express = require("express");
  const crypto = require("crypto");
  const app = express();

  app.use(express.json({ type: "*/*" }));

  const WEBHOOK_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
  MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZp06RxNzqjDAv1gxpvCkdIOnO
  BfBN12P5vWN/1pO6RQf5IYzHd6ucO+DdLUuYnVRpWOkC+GGqbeyumdlKmqeiSplZ
  cwu9ejAxRPw1xoGbm159tOYCgQaStF7w3TYsbaK7TVPDY50evtMV5IbAowgpmAkk
  fbEIgAVEf7uDIGU6LwIDAQAB
  -----END PUBLIC KEY-----`;

  app.post("/webhook", (req, res) => {
    const signature = req.headers["x-signature"];
    const rawBody = JSON.stringify(req.body);

    const isVerified = crypto.verify(
      "SHA512",
      Buffer.from(rawBody, "utf8"),
      { key: WEBHOOK_PUBLIC_KEY, padding: crypto.constants.RSA_PKCS1_PADDING },
      Buffer.from(signature, "base64")
    );

    if (!isVerified) {
      return res.status(401).send("Invalid signature");
    }

    // Process the verified event
    console.log("Received event:", req.body);

    // Always return 200 to acknowledge receipt
    res.sendStatus(200);
  });

  app.listen(3000);
  ```
</Steps>

## Webhook Payload

Every event delivers a JSON body with full transaction details. Below is an example payload for a completed withdrawal:

```json theme={null}
{
  "id": "string",
  "sub_account": {
    "id": "string",
    "title": "Main Account"
  },
  "type": "Withdraw",
  "asset": "USDT_TRX",
  "amount": 24132.32,
  "amount_in_usd": 24130,
  "network_fee": 27.29748,
  "nett": -24159.61748,
  "fees": {
    "sweep_fee": 0,
    "refill_fee": 0,
    "withdrawal_fee": 27.29748
  },
  "hash": "string",
  "source_type": "Internal",
  "source_id": "string",
  "source_alias": "Omnibus Wallet",
  "dest_type": "External",
  "dest_id": "string",
  "dest_alias": null,
  "dest_outputs": [],
  "memo": "832",
  "sequence_id": null,
  "created_on_utc": "2024-11-26T23:43:30Z",
  "created_timestamp_utc": 1732664610,
  "status": "Success",
  "confirmations": {
    "required": 3,
    "actual": 17
  },
  "explorer_url": "https://tronscan.org/#/transaction/000",
  "last_updated_on_utc": "2024-11-26T23:45:51.092536Z",
  "last_updated_timestamp_utc": 1732664751,
  "tx_key": null,
  "failure_reason": null,
  "approvals": null,
  "aml": {
    "cid": "string",
    "risk_score": null,
    "ca_payload": null
  },
  "created_by": {
    "name": "username-string",
    "email": null
  },
  "cancelled_by": null
}
```

Key fields to note:

| Field           | Description                                                      |
| --------------- | ---------------------------------------------------------------- |
| `id`            | Unique Finrock transaction ID                                    |
| `type`          | `Deposit` or `Withdraw`                                          |
| `asset`         | Asset ticker (e.g. `USDT_TRX`, `BTC`)                            |
| `amount`        | Transaction amount in the asset's native units                   |
| `amount_in_usd` | USD equivalent at time of transaction                            |
| `nett`          | Net amount after all fees (negative for withdrawals)             |
| `status`        | Current transaction status (e.g. `Success`, `Pending`, `Failed`) |
| `confirmations` | `required` and `actual` on-chain confirmations                   |
| `explorer_url`  | Direct link to the transaction on the relevant block explorer    |
| `aml.cid`       | The `aml_cid` customer ID you passed at transaction creation     |
| `sequence_id`   | The idempotency key you provided, if any                         |

## Retry Policy

<Warning>
  Your webhook endpoint **must** return an HTTP `200` status code to acknowledge successful receipt. If Finrock receives any other response code — or no response within the timeout window — it marks the delivery as failed and retries.
</Warning>

Finrock retries a failed delivery up to **5 times**, with increasing delays between each attempt:

| Attempt   | Delay After Previous |
| --------- | -------------------- |
| 1st retry | 30 seconds           |
| 2nd retry | 60 seconds           |
| 3rd retry | 90 seconds           |
| 4th retry | 120 seconds          |
| 5th retry | 180 seconds          |

Design your handler to be idempotent — if the same event is delivered more than once due to a retry, processing it twice should not cause incorrect state in your system.

## Webhook Authentication

All webhook requests include an `x-signature` header containing an RSA-SHA512 signature of the raw JSON payload body. Verify this signature using Finrock's public key before processing any event.

**Finrock Webhook Public Key:**

```
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZp06RxNzqjDAv1gxpvCkdIOnO
BfBN12P5vWN/1pO6RQf5IYzHd6ucO+DdLUuYnVRpWOkC+GGqbeyumdlKmqeiSplZ
cwu9ejAxRPw1xoGbm159tOYCgQaStF7w3TYsbaK7TVPDY50evtMV5IbAowgpmAkk
fbEIgAVEf7uDIGU6LwIDAQAB
-----END PUBLIC KEY-----
```

### Signature Verification

Use the following Node.js example to verify the signature in your webhook handler:

```javascript theme={null}
const crypto = require("crypto");

const jsonPayload = 'WEBHOOK_JSON_BODY';        // Raw request body as a string
const signature   = 'WEBHOOK_HEADER_X-SIGNATURE'; // Value of the x-signature header
const publicKey   = 'WEBHOOK_PUBLIC_KEY';         // Finrock's public key above

const isVerified = crypto.verify(
  "SHA512",
  Buffer.from(jsonPayload, "utf8"),
  {
    key: publicKey,
    padding: crypto.constants.RSA_PKCS1_PADDING,
  },
  Buffer.from(signature, "base64")
);

// isVerified is true when the signature is valid
console.log("Signature verified:", isVerified);
```

<Tip>
  Always verify the `x-signature` header before processing a webhook event. This confirms the request genuinely originated from Finrock and has not been tampered with in transit.
</Tip>

<Info>
  Make sure your framework captures the **raw** request body as a string before any JSON parsing. Some frameworks (like Express with `express.json()`) parse the body before your handler runs — use `express.raw()` or capture the raw buffer if your parsed and stringified JSON does not match the original byte sequence, which would cause signature verification to fail.
</Info>
