curl --request POST \
--url https://api.tabby.ai/api/v1/dispute-webhooks \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--header 'X-Merchant-Code: <x-merchant-code>' \
--data '
{
"url": "https://example.com/tabby/dispute-webhook"
}
'import requests
url = "https://api.tabby.ai/api/v1/dispute-webhooks"
payload = { "url": "https://example.com/tabby/dispute-webhook" }
headers = {
"X-Merchant-Code": "<x-merchant-code>",
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {
'X-Merchant-Code': '<x-merchant-code>',
Authorization: 'Bearer <token>',
'Content-Type': 'application/json'
},
body: JSON.stringify({url: 'https://example.com/tabby/dispute-webhook'})
};
fetch('https://api.tabby.ai/api/v1/dispute-webhooks', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.tabby.ai/api/v1/dispute-webhooks",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'url' => 'https://example.com/tabby/dispute-webhook'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json",
"X-Merchant-Code: <x-merchant-code>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.tabby.ai/api/v1/dispute-webhooks"
payload := strings.NewReader("{\n \"url\": \"https://example.com/tabby/dispute-webhook\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("X-Merchant-Code", "<x-merchant-code>")
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.tabby.ai/api/v1/dispute-webhooks")
.header("X-Merchant-Code", "<x-merchant-code>")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"url\": \"https://example.com/tabby/dispute-webhook\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.tabby.ai/api/v1/dispute-webhooks")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["X-Merchant-Code"] = '<x-merchant-code>'
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"url\": \"https://example.com/tabby/dispute-webhook\"\n}"
response = http.request(request)
puts response.read_body{
"id": "f5eb7c26-b163-4fad-b75f-66024824116e",
"url": "https://example.com/tabby/dispute-webhook",
"header": {
"title": "X-Auth-Key",
"value": "****cret"
}
}{
"status": "error",
"errorType": "bad_data",
"error": "bad_request"
}{
"status": "error",
"errorType": "not_authorized",
"error": "invalid secret key"
}{
"status": "error",
"errorType": "no_permission",
"error": "disputes have no test mode"
}{
"status": "error",
"errorType": "internal",
"error": "internal"
}Register a dispute webhook
Registers a dispute webhook for the merchant identified by the secret key and the
X-Merchant-Code header. Disputes have no test mode: authorize with a production
secret key (sk_...) — a request signed with a test key (sk_test_...) is rejected
with 403. Each merchant_code can have up to 4 dispute webhooks; the limit is
separate from payment webhooks. The url is normalised before it is stored
(lower-case scheme and host, default port and trailing slash removed) and must be
unique per merchant. header.value is a secret: it is accepted in full and returned
masked in every response, including this one — **** followed by the last 4 characters
for values longer than 8 characters, **** alone for shorter values.
curl --request POST \
--url https://api.tabby.ai/api/v1/dispute-webhooks \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--header 'X-Merchant-Code: <x-merchant-code>' \
--data '
{
"url": "https://example.com/tabby/dispute-webhook"
}
'import requests
url = "https://api.tabby.ai/api/v1/dispute-webhooks"
payload = { "url": "https://example.com/tabby/dispute-webhook" }
headers = {
"X-Merchant-Code": "<x-merchant-code>",
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {
'X-Merchant-Code': '<x-merchant-code>',
Authorization: 'Bearer <token>',
'Content-Type': 'application/json'
},
body: JSON.stringify({url: 'https://example.com/tabby/dispute-webhook'})
};
fetch('https://api.tabby.ai/api/v1/dispute-webhooks', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.tabby.ai/api/v1/dispute-webhooks",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'url' => 'https://example.com/tabby/dispute-webhook'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json",
"X-Merchant-Code: <x-merchant-code>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.tabby.ai/api/v1/dispute-webhooks"
payload := strings.NewReader("{\n \"url\": \"https://example.com/tabby/dispute-webhook\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("X-Merchant-Code", "<x-merchant-code>")
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.tabby.ai/api/v1/dispute-webhooks")
.header("X-Merchant-Code", "<x-merchant-code>")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"url\": \"https://example.com/tabby/dispute-webhook\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.tabby.ai/api/v1/dispute-webhooks")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["X-Merchant-Code"] = '<x-merchant-code>'
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"url\": \"https://example.com/tabby/dispute-webhook\"\n}"
response = http.request(request)
puts response.read_body{
"id": "f5eb7c26-b163-4fad-b75f-66024824116e",
"url": "https://example.com/tabby/dispute-webhook",
"header": {
"title": "X-Auth-Key",
"value": "****cret"
}
}{
"status": "error",
"errorType": "bad_data",
"error": "bad_request"
}{
"status": "error",
"errorType": "not_authorized",
"error": "invalid secret key"
}{
"status": "error",
"errorType": "no_permission",
"error": "disputes have no test mode"
}{
"status": "error",
"errorType": "internal",
"error": "internal"
}https://api.tabby.ai/api/v1/dispute-webhooks (https://api.tabby.sa/api/v1/dispute-webhooks for KSA) with your live secret key and the X-Merchant-Code header. How dispute webhooks work, payload and statuses: Dispute webhooks.Authorizations
Bearer authentication header of the form Bearer <secret_key>, where <secret_key> is your secret_key.
Headers
Merchant code the request is scoped to. Required on every dispute-webhook request, even when your secret key maps to a single merchant; case-insensitive.
"code provided to you from Tabby side"
Body
Endpoint for dispute notifications (HTTPS recommended). Must be an absolute URL with a publicly resolvable host name — localhost, raw IP addresses and hosts that do not resolve are rejected. Normalised before it is stored (lower-case scheme and host, default port and trailing slash removed) and unique per merchant.
"https://example.com/tabby/dispute-webhook"
Optional static header Tabby adds to every notification so you can verify its origin. Omit it on PUT to remove the header.
Show child attributes
Show child attributes
Response
Success. Dispute webhook object is returned; header.value is masked.
Unique dispute webhook ID, assigned by Tabby.
"f5eb7c26-b163-4fad-b75f-66024824116e"
Endpoint for dispute notifications, as stored (normalised).
"https://example.com/tabby/dispute-webhook"
The signing header, if one was registered. value is masked.
Show child attributes
Show child attributes
Was this page helpful?