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

# Webhooks

> Receive real-time notifications for transaction and verification events

Webhooks notify your application in real time when transaction events occur. Configure a webhook URL to receive updates for deposits, conversions, transfers, onramp requests, and offramp requests.

## Setup

Configure your webhook URL through the [dashboard](/v4/documentation/environments#dashboard) or programmatically via the [Update Webhook URL](/api-reference/endpoint/v3/user/update-webhook-url) endpoint.

Your endpoint must be a publicly accessible `POST` URL that returns a `200` status code immediately. Process events asynchronously after acknowledging receipt.

## Receiving events

Every webhook payload includes three fields:

| Field       | Type   | Description                                             |
| ----------- | ------ | ------------------------------------------------------- |
| `event`     | string | The event type (e.g., `Deposit`, `Convert`, `Transfer`) |
| `data`      | object | Event-specific payload                                  |
| `signature` | string | RSA signature for verification                          |

## Supported events

| Event                  | Description                                        |
| ---------------------- | -------------------------------------------------- |
| `Deposit`              | A fiat or crypto deposit was received              |
| `Convert`              | A currency conversion completed                    |
| `Transfer`             | A transfer (payout/withdrawal) status changed      |
| `Onramp`               | An onramp (fiat-to-crypto) request status changed  |
| `Withdrawal`           | A withdrawal was processed                         |
| `Offramp`              | An offramp (crypto-to-fiat) request status changed |
| `verification.success` | A user successfully completed KYC verification     |
| `verification.failed`  | A user failed KYC verification (includes reason)   |

### Sample events

<CodeGroup>
  ```json Deposit theme={null}
  {
    "event": "Deposit",
    "data": {
      "amount": 5001,
      "toUser": "chng",
      "currency": "NGN",
      "toAccount": "chng",
      "status": "completed",
      "sessionId": "3c58db9",
      "transactionMemo": "Mock Deposit"
    },
    "signature": "m9STu6pcsviYMfgu5JBjuR"
  }
  ```

  ```json Convert theme={null}
  {
    "event": "convert",
    "data": {
      "transactionReference": "a1fe4aa5679d7",
      "fromUser": "chng",
      "toAccount": "chng",
      "fromAmount": 5,
      "toAmount": 6000.05,
      "fromCurrency": "USD",
      "toCurrency": "NGN"
    },
    "signature": "gTZdvoadZscP3g=="
  }
  ```

  ```json Transfer theme={null}
  {
    "event": "Transfer",
    "data": {
      "transactionReference": "a16db1274783",
      "amount": 1,
      "fromUser": "chngr",
      "toUser": "chngr",
      "currency": "USD",
      "fromAccount": "chngr",
      "toAccount": "rhmn",
      "status": "completed"
    },
    "signature": "C73lZKC80Cg=="
  }
  ```

  ```json Onramp theme={null}
  {
    "event": "Onramp",
    "data": {
      "transactionReference": "a1e681b09cc7ccea",
      "toUser": "chngr",
      "toAccount": "chngr",
      "status": "completed",
      "sessionId": "52567c9fb6864a8c94549675c70cc9a8",
      "fromAmount": 149775,
      "toAmount": 109.99175768162324,
      "fromCurrency": "NGN",
      "toCurrency": "USDT",
      "transactionMemo": "Mock Deposit"
    },
    "signature": "Wh+Jqnxc2Iw=="
  }
  ```

  ```json Withdrawal theme={null}
  {
    "event": "Withdrawal",
    "data": {
      "transactionReference": "a1e687ccea",
      "amount": 109.9917,
      "fromUser": "chang",
      "currency": "USDT",
      "network": "tron",
      "fromAccount": "chang",
      "toAddress": "TSqRgHDE1Nypqo1LUp6Q",
      "status": "processing",
      "transactionHash": "3fa07c748260f469896bf"
    },
    "signature": "dC8YIklLc08uiGxA=="
  }
  ```

  ```json Offramp theme={null}
  {
    "event": "Offramp",
    "data": {
      "transactionReference": "691af31ba66789",
      "toUser": "chng",
      "toAccount": "chng",
      "status": "completed",
      "transactionHash": "99d7c7b5f8f3447d0cc20279f8bab2f827558c5b76",
      "fromAmount": 84.2,
      "toAmount": 99879.3927,
      "fromCurrency": "USDT",
      "toCurrency": "NGN"
    },
    "signature": "faUgOe8SYUfBlvRWsDxg=="
  }
  ```
</CodeGroup>

## Verifying signatures

Every webhook includes an RSA signature. Always verify the signature before processing an event to confirm it came from Partna.

The signature is generated by signing the JSON-encoded `data` field with Partna's RSA private key using SHA-256 with PSS padding. Verify it against the corresponding public key for your environment.

<CodeGroup>
  ```javascript Node.js theme={null}
  import crypto from "crypto";

  const SAMPLE_WEBHOOK_REQUEST = `{"event":"Convert","data":{"transactionReference":"a1632e4ea53f2802846cf7797d29bfba","fromUser":"eltnegbiz2","toAccount":"eltnegbiz2","timestamp":1775132847,"fromAmount":1,"toAmount":1000.5,"fromCurrency":"USD","toCurrency":"NGN"},"signature":"ISPOtoKOJRSuT0rKoz0P7JSPSna6UztGOrkKJKWSn62SqUobtaCYURVbhE6A8eIt82qJ9+32XlK1GJZkA5A64MYuDBLF9MdlHvfuNDkPRTof4RV+8UdlSifmBqAU2oYtvA6z4aFHUdiRcfpPNSdkRmLARwIbBxv9Zg6GHFV5HKqybMNt0QaCQskUWVcWOnCEf9PpKV1cKhJWWchYNpiCJgcV8N/vUgOFFVymhkXzbfZADuLukEwScJ751Ll97wWpLwZ1s+1DoBNQ5cY32ft80c3fwnO1NpAW/n22Bpf82rdLP1JctaJUV/3yuorMh+qNaKaXocAMpn6Vkxu8NdyNdQ=="}`;

  const PUBLIC_KEY = `-----BEGIN RSA PUBLIC KEY-----
  MIIBCgKCAQEAv2ipgHLFFHgGHr9VpsPN8V1HIbCrlmTZRU/CYSDaoVX+xerJOMGX
  qmwgQMQH5T81VaMw4rtIA8tT4DkJgjb+7G0x4CGK1OPdlvhEGP2mOFy02onkEnMv
  uN3glVc4YKLvWDTG0KT7q9mARBIkO2Nrwy6IVHAl9pMXMJTRS22c0cIbuRmkYsGZ
  trylUv50knbRSgy5EA6523+j3PPJB4TgsigGSJxJGuksaxnDQGRE558xnyw/0gJm
  mAIdbxboQTGMqod/My/kAssRkUNu1QtqrsdhZmGYHS+pIPJSaxqHEy8eiTahoqqq
  8KgNUfQfwduG+Kc4f/t5JHetSt1dgulmswIDAQAB
  -----END RSA PUBLIC KEY-----`;

  function verifySignature(data: string, signature: string) {
    return crypto.verify(
      "sha256",
      Buffer.from(JSON.stringify(data), "utf8"),
      {
        key: PUBLIC_KEY,
        padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
      },
      Buffer.from(signature, "base64"),
    );
  }

  function run() {
    const { event, data, signature } = JSON.parse(SAMPLE_WEBHOOK_REQUEST);

    const isValid = verifySignature(data, signature);

    if (isValid) {
      console.log("Signature is valid. Processing event:", event);
      // Process the event data as needed
    } else {
      console.error("Invalid signature. Possible tampering detected.");
    }
  }
  run();

  ```

  ```python Python theme={null}
  # install dependencies with `pip install cryptography`
  import base64
  import json

  from cryptography.hazmat.primitives import hashes, serialization
  from cryptography.hazmat.primitives.asymmetric import padding

  SAMPLE_WEBHOOK_REQUEST = b'{"event":"Convert","data":{"transactionReference":"a1632e4ea53f2802846cf7797d29bfba","fromUser":"eltnegbiz2","toAccount":"eltnegbiz2","timestamp":1775132847,"fromAmount":1,"toAmount":1000.5,"fromCurrency":"USD","toCurrency":"NGN"},"signature":"ISPOtoKOJRSuT0rKoz0P7JSPSna6UztGOrkKJKWSn62SqUobtaCYURVbhE6A8eIt82qJ9+32XlK1GJZkA5A64MYuDBLF9MdlHvfuNDkPRTof4RV+8UdlSifmBqAU2oYtvA6z4aFHUdiRcfpPNSdkRmLARwIbBxv9Zg6GHFV5HKqybMNt0QaCQskUWVcWOnCEf9PpKV1cKhJWWchYNpiCJgcV8N/vUgOFFVymhkXzbfZADuLukEwScJ751Ll97wWpLwZ1s+1DoBNQ5cY32ft80c3fwnO1NpAW/n22Bpf82rdLP1JctaJUV/3yuorMh+qNaKaXocAMpn6Vkxu8NdyNdQ=="}'

  PUBLIC_KEY_PEM = b"""-----BEGIN RSA PUBLIC KEY-----
  MIIBCgKCAQEAv2ipgHLFFHgGHr9VpsPN8V1HIbCrlmTZRU/CYSDaoVX+xerJOMGX
  qmwgQMQH5T81VaMw4rtIA8tT4DkJgjb+7G0x4CGK1OPdlvhEGP2mOFy02onkEnMv
  uN3glVc4YKLvWDTG0KT7q9mARBIkO2Nrwy6IVHAl9pMXMJTRS22c0cIbuRmkYsGZ
  trylUv50knbRSgy5EA6523+j3PPJB4TgsigGSJxJGuksaxnDQGRE558xnyw/0gJm
  mAIdbxboQTGMqod/My/kAssRkUNu1QtqrsdhZmGYHS+pIPJSaxqHEy8eiTahoqqq
  8KgNUfQfwduG+Kc4f/t5JHetSt1dgulmswIDAQAB
  -----END RSA PUBLIC KEY-----"""


  def verify_signature(payload: bytes, signature: str) -> bool:
      public_key = serialization.load_pem_public_key(PUBLIC_KEY_PEM)
      sig_bytes = base64.b64decode(signature)
      try:
          public_key.verify(
              sig_bytes,
              payload,
              padding.PSS(
                  mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH
              ),
              hashes.SHA256(),
          )
          return True
      except Exception:
          return False


  def main():
      json_data = json.loads(SAMPLE_WEBHOOK_REQUEST)
      event = json_data["event"]
      signature = json_data["signature"]

      # extract data bytes
      payload = SAMPLE_WEBHOOK_REQUEST[
          SAMPLE_WEBHOOK_REQUEST.index(b'"data":') + 7
          : 
          SAMPLE_WEBHOOK_REQUEST.index(b"}") + 1
      ]

      if verify_signature(payload, signature):
          print(f"Signature valid for event: {event}")
          # Process the event data as needed
      else:
          print("Invalid signature")


  if __name__ == "__main__":
      main()

  ```

  ```go Go theme={null}
  package main

  import (
  	"crypto"
  	"crypto/rsa"
  	"crypto/sha256"
  	"crypto/x509"
  	"encoding/base64"
  	"encoding/json"
  	"encoding/pem"
  	"fmt"
  )

  var (
  	SAMPLE_WEBHOOK_REQUEST = `{"event":"Convert","data":{"transactionReference":"a1632e4ea53f2802846cf7797d29bfba","fromUser":"eltnegbiz2","toAccount":"eltnegbiz2","timestamp":1775132847,"fromAmount":1,"toAmount":1000.5,"fromCurrency":"USD","toCurrency":"NGN"},"signature":"ISPOtoKOJRSuT0rKoz0P7JSPSna6UztGOrkKJKWSn62SqUobtaCYURVbhE6A8eIt82qJ9+32XlK1GJZkA5A64MYuDBLF9MdlHvfuNDkPRTof4RV+8UdlSifmBqAU2oYtvA6z4aFHUdiRcfpPNSdkRmLARwIbBxv9Zg6GHFV5HKqybMNt0QaCQskUWVcWOnCEf9PpKV1cKhJWWchYNpiCJgcV8N/vUgOFFVymhkXzbfZADuLukEwScJ751Ll97wWpLwZ1s+1DoBNQ5cY32ft80c3fwnO1NpAW/n22Bpf82rdLP1JctaJUV/3yuorMh+qNaKaXocAMpn6Vkxu8NdyNdQ=="}`

  	PUBLIC_KEY_PEM = `-----BEGIN RSA PUBLIC KEY-----
  MIIBCgKCAQEAv2ipgHLFFHgGHr9VpsPN8V1HIbCrlmTZRU/CYSDaoVX+xerJOMGX
  qmwgQMQH5T81VaMw4rtIA8tT4DkJgjb+7G0x4CGK1OPdlvhEGP2mOFy02onkEnMv
  uN3glVc4YKLvWDTG0KT7q9mARBIkO2Nrwy6IVHAl9pMXMJTRS22c0cIbuRmkYsGZ
  trylUv50knbRSgy5EA6523+j3PPJB4TgsigGSJxJGuksaxnDQGRE558xnyw/0gJm
  mAIdbxboQTGMqod/My/kAssRkUNu1QtqrsdhZmGYHS+pIPJSaxqHEy8eiTahoqqq
  8KgNUfQfwduG+Kc4f/t5JHetSt1dgulmswIDAQAB
  -----END RSA PUBLIC KEY-----`
  )

  func VerifyData(data []byte, signature string) bool {
  	sig, err := base64.StdEncoding.DecodeString(signature)
  	if err != nil {
  		fmt.Println(err)
  		return false
  	}

  	block, _ := pem.Decode([]byte(PUBLIC_KEY_PEM))
  	if block == nil {
  		fmt.Println("failed to parse PEM block containing the public key")
  		return false
  	}

  	pubkey, _ := x509.ParsePKCS1PublicKey(block.Bytes)

  	msgHash := sha256.New()
  	msgHash.Write(data)
  	msgHashSum := msgHash.Sum(nil)

  	err = rsa.VerifyPSS(pubkey, crypto.SHA256, msgHashSum, sig, nil)

  	return err == nil
  }

  func main() {
  	var data struct {
  		Event     string          `json:"event"`
  		Data      json.RawMessage `json:"data"`
  		Signature string          `json:"signature"`
  	}

  	json.Unmarshal([]byte(SAMPLE_WEBHOOK_REQUEST), &data)

  	isValid := VerifyData(data.Data, data.Signature)
  	if isValid {
  		fmt.Println("Signature is valid")
  	} else {
  		fmt.Println("Signature is invalid")
  	}
  }

  ```
</CodeGroup>

## Public keys

Use the correct public key for your environment.

### Staging

```text theme={null}
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAv2ipgHLFFHgGHr9VpsPN8V1HIbCrlmTZRU/CYSDaoVX+xerJOMGX
qmwgQMQH5T81VaMw4rtIA8tT4DkJgjb+7G0x4CGK1OPdlvhEGP2mOFy02onkEnMv
uN3glVc4YKLvWDTG0KT7q9mARBIkO2Nrwy6IVHAl9pMXMJTRS22c0cIbuRmkYsGZ
trylUv50knbRSgy5EA6523+j3PPJB4TgsigGSJxJGuksaxnDQGRE558xnyw/0gJm
mAIdbxboQTGMqod/My/kAssRkUNu1QtqrsdhZmGYHS+pIPJSaxqHEy8eiTahoqqq
8KgNUfQfwduG+Kc4f/t5JHetSt1dgulmswIDAQAB
-----END RSA PUBLIC KEY-----
```

### Production

```text theme={null}
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAoGmOqVUxIGq92P8J5KqwJbwucsNeUwfS76saj0UPgT1IsK/mvceR
Ges7dsgE4EVx7Wsd5PPNeAfaNt8d0plBLhHRW64WFyv6jYcYp8eVdHUxWLA6p5gZ
9rGrZiwqKqunppTPlV04gdDC32rAbpAR3IMYmMJLuPy63Oumszl4qk69A1o60Son
r0KYBaRK7aQsFT9IFexicDUhrF1SohaNH/msTdvJb0SwSGiV92EhmmC2R8CL83/B
S8QC5c9PWtZ4a26CRLHe0IfaGGz8ClhlO8IFxz0cNrpoRa4JRsffezQ+RMQqCxhc
igC7wHfqZr5BtziQOYJjUknVzsd81HEYlwIDAQAB
-----END RSA PUBLIC KEY-----
```

## Best practices

**Always verify signatures.** Never process a webhook event without verifying the signature first. This prevents spoofed events from affecting your system.

**Return 200 immediately.** Acknowledge receipt before processing. If your endpoint takes too long to respond, the delivery may be retried.

**Handle duplicates.** Use the `sessionId` or `rampReference` in the event data as an idempotency key. The same event may be delivered more than once.

**Log raw payloads.** Store the raw webhook body for debugging. If something goes wrong, the raw payload is essential for troubleshooting.
