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

# Set up a callback or webhook endpoint

> Step-by-step guide to setting up a webhook endpoint in WaaS 2.0 for real-time event notifications.

<Tip>
  Try [Cobo WaaS Skill](/v2/guides/overview/cobo-waas-skill) in your AI coding assistant (Claude Code, Cursor, etc.). Describe your needs in natural language to auto-generate production-ready SDK code and debug faster 🚀
</Tip>

Webhook events and callback messages are crucial for ensuring seamless data integration and communication between the WaaS service and your application. This guide provides an overview of how to create and manage an endpoint for receiving and processing webhook events and callback messages.

## Create an endpoint

First, choose a server environment, such as a cloud service like AWS, Google Cloud, or a self-hosted server, that supports receiving and processing webhook events or callback messages. Then, define an endpoint URL on your server where the webhook events and callback messages will be sent.

## Implement handling logic

After you create the endpoint, you need to implement the logic on the server to handle the webhook events or callback messages, including parsing the API request, verifying the signature, responding to the request and adding other handling logic if necessary.

### Verify the signature

To prevent unauthorized access, when you receive a webhook event or a callback message, you need to validate the authenticity of the API request by verifying the signature.

The verification steps are as follows:

1. Retrieve raw body and timestamp.

   Extract the original body string from the request payload and the timestamp from the request headers.

   ```python theme={null}
   raw_body = request.body().decode('utf8')
   timestamp = request.headers.get("BIZ_TIMESTAMP")
   ```

2. Retrieve the signature.

   Fetch the signature value from the request header.

   ```python theme={null}
   signature = request.headers.get('BIZ_RESP_SIGNATURE')
   ```

3. Concatenate and hash the message.

   ```python theme={null}
   import hashlib

   # Concatenate raw body and timestamp to form the message.
   message = "raw_body|timestamp"

   # Compute double SHA-256 hash.
   sha256_hash = hashlib.sha256(hashlib.sha256(message.encode()).digest()).digest()
   ```

4. Select Cobo's Public Key.

   Depending on the environment that you use, select the corresponding public key for verification:

   * Development environment: `a04ea1d5fa8da71f1dcfccf972b9c4eba0a2d8aba1f6da26f49977b08a0d2718`
   * Production environment: `8d4a482641adb2a34b726f05827dba9a9653e5857469b8749052bf4458a86729`

5. Verify the signature using the Ed25519 algorithm.

   ```python theme={null}
   import ed25519

   # Obtain the verifying key from Cobo's public key.
   vk = ed25519.VerifyingKey(bytes.fromhex(public_key)) 

   # Verify the signature against the computed message hash.
   vk.verify(bytes.fromhex(signature), sha256_hash)
   ```

### Respond to the API request

Properly responding to webhook events and callback messages is crucial for ensuring that webhooks and callbacks are processed as expected. This section describes the expected response from both webhook and callback endpoints.

#### Webhook events

When your webhook endpoint receives a webhook event, it should respond with a status code of `200` or `201` to indicate that the event has been successfully received and processed. Once this response is sent, the WaaS service will stop retrying to send the event and the event status will become `Delivered` on Cobo Portal.

The default timeout for each webhook event is 2 seconds. If the webhook endpoint does not respond or responds with a status code other than `200` or `201`, the WaaS service will continue to retry sending the event. If the number of retry attempts reaches 10 , the WaaS service will stop sending the event and the event status will become `Failed`· You can resend the event by clicking **Retry** on **Cobo Portal** > **Developer** > **WaaS 2.0** > **Webhook Events**.

Cobo does not guarantee that events will be delivered in the order they are generated. For example, creating a transfer will generate the following events:

* `wallets.transaction.created`
* `wallets.transaction.updated`
* `wallets.transaction.succeeded`

Your endpoint should not assume that events will arrive in this sequence and must handle delivery appropriately.

#### Callback messages

When your callback endpoint receives a callback message, it should respond with a status code of `200` or `201` and a response body of `ok` or `deny` to indicate transaction approval or rejection. Once this response is sent, the WaaS service will stop retrying to send the message and the callback message status will become `Delivered`on Cobo Portal.

If the callback endpoint does not respond, responds with a status code other than `200` or `201`,  or the response body does not contain `ok` or `deny`, the WaaS service will continue to retry sending the message.  If the number of retry attempts reaches 30, the WaaS service will stop sending the message and the callback message status will become `Failed`. You can resend the message by using the [Retry callback message](/v2/api-references/developers/retry-callback-message) operation.

### Common delivery failures

The following table lists the most frequent reasons why callback messages or webhook events fail to deliver, and how to fix them.

| Symptom                                                                                                                    | Root cause                                                                          | Fix                                                                                                                                                                               |
| -------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Callback message status is `Retrying` or `Failed`; response body in the Portal log shows `{"result":"ok"}` or similar JSON | Response body is JSON instead of plain text                                         | Change your response body to the exact string `ok` or `deny` — no JSON wrapping, no quotes.                                                                                       |
| Callback message shows no delivery attempts for a new URL                                                                  | Endpoint not publicly reachable (e.g. ngrok tunnel is not running or has restarted) | Start or restart your ngrok tunnel and verify the URL is reachable from outside your local network before registering.                                                            |
| Webhook events show `status_code: 301` or `302` in delivery logs                                                           | Server returned a redirect                                                          | Serve the response directly from the registered URL. Cobo does not follow redirects.                                                                                              |
| Delivery attempts fail with TLS or SSL error                                                                               | Endpoint TLS certificate is invalid or expired                                      | Renew the certificate on your server. The endpoint must use HTTPS with a valid certificate.                                                                                       |
| Events or messages stop arriving after a period                                                                            | Retry limit exhausted                                                               | Webhook events retry up to 10 times; callback messages retry up to 30 times. After the limit is reached, status becomes `Failed`. Manually retry from Cobo Portal or via the API. |

<Note>
  You can inspect the detailed response body and status code for each delivery attempt in Cobo Portal under **Developer** > **Webhook Events** / **Callback Messages** > click an event or message > **Delivery logs**.
</Note>

### Code samples

To see examples of how to implement the handling logic, refer to the following files in the WaaS SDK GitHub repository:

* Python: [server\_demo.py](https://github.com/CoboGlobal/cobo-waas2-python-sdk/blob/master/cobo_waas2/server_demo.py) (implemented based on the FastAPI framework)
* Java: [DemoController.java](https://github.com/CoboGlobal/cobo-waas2-java-sdk/blob/master/src/main/java/com/cobo/waas2/demo/DemoController.java) (implemented based on the SpringBoot framework)
* JavaScript: [ServerDemo.js](https://github.com/CoboGlobal/cobo-waas2-js-sdk/blob/master/src/ServerDemo.js)
* Go: [server\_demo.go](https://github.com/CoboGlobal/cobo-waas2-go-sdk/blob/master/cobo_waas2/demo/server_demo.go)

## Advanced usage

### Wallet-level webhook routing

In certain business scenarios, you may need to apply different Webhook handling logic for different wallets. For example:

* Multiple business teams share the same WaaS account
* A single system manages multiple independent projects
* Webhook events need to be forwarded to different microservices

In such cases, you can route incoming Webhook events based on the `wallet_id`.
The following example demonstrates how to extend the Cobo-provided Webhook/Callback sample code ([server\_demo.py](https://github.com/CoboGlobal/cobo-waas2-python-sdk/blob/master/cobo_waas2/server_demo.py)) to implement wallet-level Webhook routing.

This example uses Python for demonstration, but the same logic can be applied to other languages. You may adapt this approach in the sample code of the language you are using.

```python theme={null}
@app.post("/api/webhook")
async def handle_webhook(
    request: Request,
    biz_timestamp: Optional[str] = Header(None),
    biz_resp_signature: Optional[str] = Header(None),
):
    raw_body = await request.body()
    sig_valid = verify_signature(
        pubkey,
        biz_resp_signature,
        f"{raw_body.decode('utf8')}|{biz_timestamp}"
    )
    if not sig_valid:
        raise HTTPException(status_code=401, detail="Signature verification failed")

    import requests
    event = WebhookEvent.from_dict(json.loads(raw_body.decode('utf8')))
    data = event.data.actual_instance

    # Route Webhook events based on Wallet ID
    # Replace "wallet_id_xxx" and the target URLs with your actual wallet IDs
    # and the corresponding Webhook endpoints in your own system.
    if data.data_type == "Transaction":
        if data.wallet_id == "wallet_id_1":
            # Example: Forward events for wallet_id_1 to Service A
            requests.post("http://wallet1.example.com/webhook", json=data.model_dump())

        elif data.wallet_id == "wallet_id_2":
            # Example: Forward events for wallet_id_2 to Service B
            requests.post("http://wallet2.example.com/webhook", json=data.model_dump())

        elif data.wallet_id == "wallet_id_3":
            # Example: Forward events for wallet_id_3 to Service C
            requests.post("http://wallet3.example.com/webhook", json=data.model_dump())

        else:
            # Default handling logic (optional)
            pass

    logger.info(event)
    logger.info(event.data)
```

## ⚠️⚠️⚠️Important notes

<Warning>
  * When receiving the webhook events, your endpoint should first return the correct status code promptly and then handle any subsequent processing asynchronously to prevent timeouts.
  * Due to the retry mechanism of webhook events, webhook endpoints may sometimes receive the same event multiple times. To protect against duplicate event processing, please log the event IDs, transaction hashes, or transaction IDs you've already processed and refrain from processing those that are already logged.
</Warning>
