# Set your webhook URL

`PUT https://api.weavpayments.com/v1/webhook-endpoint`

Sets or replaces this mode's webhook URL, and optionally a separate URL for withdrawal events. They can only be set here, not in the dashboard, so nobody changes them by accident; every change is emailed to your Owners and Developers.
Each webhook goes to exactly one URL: a payment's own `webhook_url` if it has one, else `withdrawals_url` for withdrawal events when set, else url (limited to events).
The URL must be public HTTPS: private and internal addresses are refused. Webhooks are signed with the signing key you create in the dashboard, and none are sent until one exists (`has_signing_key`).

## Where each webhook goes

Every webhook goes to exactly one URL, so one event never goes to two of your URLs. Weav picks it in this order:

1. **The payment’s `webhook_url`.** A payment created with one sends all its events there: completed, failed, canceled, reversed and refunded. Your `url` and its `events` list don’t apply to that payment.
2. **`withdrawals_url`, for withdrawal events.** When it’s set, `WITHDRAWAL_COMPLETED` and `WITHDRAWAL_FAILED` go there, and `url` no longer gets them.
3. **`url`, for everything else**, limited to the `events` you chose.

| Event | Payment has a webhook_url | withdrawals_url is set | Goes to |
| --- | --- | --- | --- |
| A payment event | Yes | (doesn’t matter) | `webhook_url` only |
| A payment event | No | (doesn’t matter) | `url`, if it’s in `events` |
| A withdrawal event | (not a payment) | Yes | `withdrawals_url` only |
| A withdrawal event | (not a payment) | No | `url`, if it’s in `events` |

- Every URL follows the same rules: public HTTPS only (private and internal addresses are refused when you set it, and checked again before each delivery), signed with your signing key (see [Verifying signatures](https://docs.weavpayments.com/webhooks/verifying-signatures)), retried the same way, and shown under Developers in the dashboard with its deliveries.
- Live and test keys each set their own mode’s URLs.
- Every change to `url` or `withdrawals_url` is emailed to your account’s Owners and Developers, with the API key that made it.
- In live mode, if a URL keeps failing, your Owners, Developers and technical contact get an email: when nothing has gotten through for 10 minutes, when it’s down for an hour, a reminder if it’s still down a day later, and one when it works again. If about a quarter of webhooks fail while others get through, they get one email about that too. A short blip sends nothing. Set the technical contact on the Developers page of your dashboard.
- Nothing is sent until you create a signing key in the dashboard under Developers. Until then, `has_signing_key` is `false`.

## Headers

- `Authorization` (string, required): `Bearer` plus your API key. See [Authentication](https://docs.weavpayments.com/authentication).

## Body

JSON, with `Content-Type: application/json`.

- `url` (string (uri), required): Required. A public HTTPS URL, up to 2,048 characters. Its host must resolve. Private and internal addresses (such as `localhost` or `10.x`) are refused. Up to 2048 characters.
- `events` (array of strings): Which events url gets. Leave out for all of them. Doesn't limit `withdrawals_url` or a payment's `webhook_url`. One of: `PAYMENT_COMPLETED`, `PAYMENT_FAILED`, `PAYMENT_CANCELLED`, `PAYMENT_REVERSED`, `PAYMENT_REFUNDED`, `CHECKOUT_COMPLETED`, `CHECKOUT_EXPIRED`, `PAYMENT_METHOD_SAVED`, `PAYMENT_METHOD_FAILED`, `PAYMENT_METHOD_REMOVED`, `WITHDRAWAL_COMPLETED`, `WITHDRAWAL_FAILED`.
- `withdrawals_url` (string | null): Optional. Withdrawal events go here instead of url (url stops getting them). Null removes it; leaving it out keeps it. Up to 2048 characters.

## Responses

### 200 OK

Saved (code 2000000).

What `data` holds:

- `webhook_endpoint` (object)
  - `url` (string)
  - `events` (array of strings)
  - `health` (string): One of: `UNKNOWN`, `HEALTHY`, `FAILING`, `DOWN`.
  - `withdrawals_url` (string | null): Where withdrawal events go, when not url.
  - `has_signing_key` (boolean): Webhooks go out only once a signing key exists. Create it in the dashboard under Developers.
  - `last_success_at` (string (date-time) | null)
  - `updated_at` (string (date-time) | null)

```json
{
    "code": "2000000",
    "success": true,
    "message": "Webhook endpoint saved",
    "data": {
        "webhook_endpoint": {
            "url": "https://example.com/webhooks/weav",
            "events": [
                "PAYMENT_COMPLETED",
                "PAYMENT_FAILED",
                "PAYMENT_CANCELLED",
                "PAYMENT_REVERSED",
                "PAYMENT_REFUNDED",
                "CHECKOUT_COMPLETED",
                "CHECKOUT_EXPIRED",
                "WITHDRAWAL_COMPLETED",
                "WITHDRAWAL_FAILED"
            ],
            "withdrawals_url": null,
            "health": "UNKNOWN",
            "has_signing_key": true,
            "last_success_at": null,
            "updated_at": "2026-10-26T10:00:00+08:00"
        }
    }
}
```

### 401 Unauthorized

Missing, malformed or unknown API key (code 4010000).

```json
{
    "code": "4010000",
    "success": false,
    "message": "Missing or invalid API key."
}
```

### 403 Forbidden

The key can't make this request (code 4030000): an address outside the key's IP allowlist, a live key before your live mode is on, or an account that isn't active. A live key sent from a browser is refused (code 4030001): keep live keys on your server.

```json
{
    "code": "4030000",
    "success": false,
    "message": "Requests with this key must come from an address on its allowlist."
}
```

### 422 Unprocessable Content

One or more fields are invalid (code 4220000), or the channel is off (4220101) or doesn't take the currency (4220102).

```json
{
    "code": "4220000",
    "success": false,
    "message": "The given data was invalid.",
    "errors": {
        "channel": [
            "The channel field is required."
        ]
    }
}
```

## Examples

cURL:

```shell
curl -X PUT https://api.weavpayments.com/v1/webhook-endpoint \
    -H "Authorization: Bearer $WEAV_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
    "url": "https://example.com/webhooks/weav",
    "events": [
        "PAYMENT_COMPLETED"
    ],
    "withdrawals_url": null
}'
```

Node.js:

```js
const url = 'https://api.weavpayments.com/v1/webhook-endpoint';
const response = await fetch(url, {
    method: 'PUT',
    headers: {
        Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
        'Content-Type': 'application/json',
    },
    body: JSON.stringify({
        url: 'https://example.com/webhooks/weav',
        events: ['PAYMENT_COMPLETED'],
        withdrawals_url: null,
    }),
});

const result = await response.json();
```

JavaScript:

```js
import axios from 'axios';

const response = await axios.request({
    method: 'put',
    url: 'https://api.weavpayments.com/v1/webhook-endpoint',
    headers: {
        Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
    },
    data: {
        url: 'https://example.com/webhooks/weav',
        events: ['PAYMENT_COMPLETED'],
        withdrawals_url: null,
    },
    // Read error answers too: their body has the error code.
    validateStatus: () => true,
});

const result = response.data;
```

PHP:

```php
use Illuminate\Support\Facades\Http;

$response = Http::withToken(config('services.weav.key'))
    ->put('https://api.weavpayments.com/v1/webhook-endpoint', [
        'url' => 'https://example.com/webhooks/weav',
        'events' => [
            'PAYMENT_COMPLETED',
        ],
        'withdrawals_url' => null,
    ]);

$result = $response->json();
```

Python:

```python
import os
import requests

response = requests.put(
    "https://api.weavpayments.com/v1/webhook-endpoint",
    headers={
        "Authorization": f"Bearer {os.environ['WEAV_API_KEY']}",
    },
    json={
        "url": "https://example.com/webhooks/weav",
        "events": [
            "PAYMENT_COMPLETED",
        ],
        "withdrawals_url": None,
    },
    timeout=30,
)

result = response.json()
```

Go:

```go
package main

import (
    "fmt"
    "io"
    "net/http"
    "os"
    "strings"
)

func main() {
    body := strings.NewReader(`{
    "url": "https://example.com/webhooks/weav",
    "events": [
        "PAYMENT_COMPLETED"
    ],
    "withdrawals_url": null
}`)

    req, err := http.NewRequest("PUT", "https://api.weavpayments.com/v1/webhook-endpoint", body)
    if err != nil {
        panic(err)
    }
    req.Header.Set("Authorization", "Bearer "+os.Getenv("WEAV_API_KEY"))
    req.Header.Set("Content-Type", "application/json")

    res, err := http.DefaultClient.Do(req)
    if err != nil {
        panic(err)
    }
    defer res.Body.Close()

    result, err := io.ReadAll(res.Body)
    if err != nil {
        panic(err)
    }
    fmt.Println(res.Status, string(result))
}
```

Rust:

```rust
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }, serde_json = "1"
use serde_json::json;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let key = std::env::var("WEAV_API_KEY")?;
    let body = json!({
        "url": "https://example.com/webhooks/weav",
        "events": [
            "PAYMENT_COMPLETED"
        ],
        "withdrawals_url": null
    });

    let response = reqwest::blocking::Client::new()
        .put("https://api.weavpayments.com/v1/webhook-endpoint")
        .bearer_auth(key)
        .json(&body)
        .send()?;

    let status = response.status();
    let result: serde_json::Value = response.json()?;
    println!("{status} {result:#}");
    Ok(())
}
```

Java:

```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class WeavExample {
    public static void main(String[] args) throws Exception {
        String url = "https://api.weavpayments.com/v1/webhook-endpoint";
        String body =
                """
                {
                    "url": "https://example.com/webhooks/weav",
                    "events": [
                        "PAYMENT_COMPLETED"
                    ],
                    "withdrawals_url": null
                }
                """;

        HttpRequest request =
                HttpRequest.newBuilder()
                        .uri(URI.create(url))
                        .header("Authorization", "Bearer " + System.getenv("WEAV_API_KEY"))
                        .header("Content-Type", "application/json")
                        .PUT(HttpRequest.BodyPublishers.ofString(body))
                        .build();

        HttpResponse<String> response =
                HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
        System.out.println(response.statusCode() + " " + response.body());
    }
}
```
