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

# Sweep Webhooks

> Real-time notifications for transfers on deposit addresses.

Successful transactions on deposit addresses are notified by lifecycle stage. Both directions are notified: `is_send=false` incoming, `is_send=true` outgoing.

## Signature Verification

Request headers:

```
Content-Type: application/json
Signature: <base64_ed25519_signature>
Signature-Algorithm: Ed25519
```

Verification public key (Ed25519):

```
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOeJEPa2w1I10acMaQlng0BnGtfBwxDnM9lVHJGe+h5j
```

The signature covers the raw request body bytes, exactly as received - do not re-serialize the JSON before verifying:

```
Ed25519.Verify(public_key, raw_request_body, base64_decode(Signature))
```

<CodeGroup>
  ```go Go theme={null}
  package main

  import (
  	"bytes"
  	"crypto/ed25519"
  	"encoding/base64"
  	"strings"

  	"golang.org/x/crypto/ssh"
  )

  const publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOeJEPa2w1I10acMaQlng0BnGtfBwxDnM9lVHJGe+h5j"

  func verify(signatureB64 string, rawBody []byte) bool {
  	parsed, _, _, rest, err := ssh.ParseAuthorizedKey([]byte(publicKey))
  	if err != nil ||
  		parsed.Type() != ssh.KeyAlgoED25519 ||
  		len(bytes.TrimSpace(rest)) != 0 {
  		return false
  	}

  	cryptoKey, ok := parsed.(ssh.CryptoPublicKey)
  	if !ok {
  		return false
  	}

  	pub, ok := cryptoKey.CryptoPublicKey().(ed25519.PublicKey)
  	if !ok || len(pub) != ed25519.PublicKeySize {
  		return false
  	}

  	signature, err := base64.StdEncoding.DecodeString(
  		strings.TrimSpace(signatureB64),
  	)
  	if err != nil || len(signature) != ed25519.SignatureSize {
  		return false
  	}

  	return ed25519.Verify(pub, rawBody, signature)
  }
  ```

  ```python Python theme={null}
  import base64

  from cryptography.exceptions import InvalidSignature
  from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
  from cryptography.hazmat.primitives.serialization import load_ssh_public_key

  PUBLIC_KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOeJEPa2w1I10acMaQlng0BnGtfBwxDnM9lVHJGe+h5j"

  public_key = load_ssh_public_key(PUBLIC_KEY.encode())
  assert isinstance(public_key, Ed25519PublicKey)


  def verify(signature_b64: str, raw_body: bytes) -> bool:
      try:
          public_key.verify(base64.b64decode(signature_b64), raw_body)
          return True
      except (InvalidSignature, ValueError):
          return False
  ```

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

  const PUBLIC_KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOeJEPa2w1I10acMaQlng0BnGtfBwxDnM9lVHJGe+h5j";

  function parseSshEd25519(line) {
    const blob = Buffer.from(line.split(" ")[1], "base64");
    const algoLen = blob.readUInt32BE(0);
    const algo = blob.subarray(4, 4 + algoLen).toString();
    const keyLen = blob.readUInt32BE(4 + algoLen);
    const raw = blob.subarray(8 + algoLen, 8 + algoLen + keyLen);
    if (algo !== "ssh-ed25519" || raw.length !== 32) throw new Error("not an ssh-ed25519 key");
    return crypto.createPublicKey({
      key: Buffer.concat([Buffer.from("302a300506032b6570032100", "hex"), raw]),
      format: "der",
      type: "spki",
    });
  }

  const publicKey = parseSshEd25519(PUBLIC_KEY);

  function verify(signatureB64, rawBody) {
    const signature = Buffer.from(signatureB64.trim(), "base64");
    if (signature.length !== 64) return false;
    return crypto.verify(null, rawBody, publicKey, signature);
  }
  ```
</CodeGroup>

## Request Body

```json theme={null}
{
  "wallet_address": "0x59b4a4fd7bc1baa0e6bb65cbfe3e2e4dbfa5e0a1",
  "network": "ETHEREUM",
  "unique_id": "chain:block:hash:index",
  "block_number": 123456789,
  "block_hash": "0xabc...",
  "hash": "0xdef...",
  "is_send": false,
  "from_address": "0x1234...",
  "to_address": "0x59b4a4fd7bc1baa0e6bb65cbfe3e2e4dbfa5e0a1",
  "mint_address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
  "decimals": 6,
  "amount": "100.25",
  "status": "SUCCESS",
  "event_type": "CONFIRMED",
  "timestamp": 1784515200000,
  "uuid": "5f0c8f6e-..."
}
```

| Field                         | Description                                                                                   |
| ----------------------------- | --------------------------------------------------------------------------------------------- |
| `wallet_address`              | Deposit address                                                                               |
| `network`                     | `ETHEREUM` `ARBITRUM` `BASE` `POLYGON` `BSC` `SOLANA`; EVM deposits report the specific chain |
| `unique_id`                   | Unique on-chain event ID, shared across all stages of a transaction                           |
| `block_number` / `block_hash` | Block number (slot on Solana) / block hash                                                    |
| `hash`                        | Transaction hash (signature on Solana)                                                        |
| `is_send`                     | `true` when the transfer is outgoing from the deposit address                                 |
| `from_address` / `to_address` | Omitted when empty                                                                            |
| `mint_address`                | Token contract / mint address; empty for native coins                                         |
| `decimals`                    | Token decimals                                                                                |
| `amount`                      | Human-readable amount string                                                                  |
| `status`                      | Only `SUCCESS` is delivered                                                                   |
| `event_type`                  | See [Lifecycle Events](#lifecycle-events)                                                     |
| `timestamp`                   | On-chain timestamp (ms)                                                                       |
| `uuid`                        | Transaction record UUID, identical across stages                                              |

## Lifecycle Events

* **`CONFIRMED`** - Crediting threshold reached; safe to credit the user
* **`FINALIZED`** - Final; safe to release withdrawals. No further changes
* **`REVERTED`** - A `CONFIRMED` transaction was rolled back by a reorg (EVM only); reverse the credit. `CONFIRMED` is sent again if re-included

Notifications for a transaction are delivered strictly in order. Thresholds:

| Network  | `CONFIRMED`          | `FINALIZED`           |
| -------- | -------------------- | --------------------- |
| ETHEREUM | 6 confirmations      | 64 confirmations      |
| ARBITRUM | 128 L2 confirmations | Node finalized        |
| BASE     | 20 L2 confirmations  | Node finalized        |
| POLYGON  | 60 confirmations     | 300 confirmations     |
| BSC      | 60 confirmations     | 300 confirmations     |
| SOLANA   | -                    | Transaction finalized |

Solana sends `FINALIZED` only; it serves as both the crediting and withdrawal basis.

<Warning>
  These thresholds are MPCVault's current operating standard, provided for reference only. They may change with network conditions and do not guarantee irreversibility: a `CONFIRMED` transaction can still be rolled back by a reorg (delivered as `REVERTED`), and only `FINALIZED` is final. Crediting or releasing funds based on `CONFIRMED` is your own risk decision - MPCVault is not liable for losses caused by acting before `FINALIZED`. Apply stricter thresholds per your own risk requirements.
</Warning>

## Response and Retry

Return HTTP 200; only 200 counts as delivered (any response body is accepted). Otherwise MPCVault retries with exponential backoff (up to 1-hour intervals) until it receives 200. Suggested idempotency key:

```
mpcvault:sweep:<uuid>:<event_type>:<block_hash>
```

All stages of a transaction share the same `unique_id`, so deduplicating on `unique_id` alone would drop later stages.

## Recommended Handling Steps

1. Read the raw request body and verify the signature with the public key.
2. Parse the JSON and check that `status` is `SUCCESS`.
3. Deduplicate by `uuid` + `event_type` + `block_hash`.
4. Apply by `event_type`: `CONFIRMED` → credit the user; `REVERTED` → reverse the credit; `FINALIZED` → allow withdrawal.
5. Return HTTP 200.
