curl --request PATCH \
--url https://api.paj.cash/pub/v2/webhook \
--header 'Content-Type: application/json' \
--header 'x-api-key: <api-key>' \
--data '
{
"rampWebhookURL": "https://example.com/webhook",
"paymentWebhookURL": "https://example.com/payment-webhook"
}
'import requests
url = "https://api.paj.cash/pub/v2/webhook"
payload = {
"rampWebhookURL": "https://example.com/webhook",
"paymentWebhookURL": "https://example.com/payment-webhook"
}
headers = {
"x-api-key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.patch(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'PATCH',
headers: {'x-api-key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
rampWebhookURL: 'https://example.com/webhook',
paymentWebhookURL: 'https://example.com/payment-webhook'
})
};
fetch('https://api.paj.cash/pub/v2/webhook', 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.paj.cash/pub/v2/webhook",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "PATCH",
CURLOPT_POSTFIELDS => json_encode([
'rampWebhookURL' => 'https://example.com/webhook',
'paymentWebhookURL' => 'https://example.com/payment-webhook'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"x-api-key: <api-key>"
],
]);
$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.paj.cash/pub/v2/webhook"
payload := strings.NewReader("{\n \"rampWebhookURL\": \"https://example.com/webhook\",\n \"paymentWebhookURL\": \"https://example.com/payment-webhook\"\n}")
req, _ := http.NewRequest("PATCH", url, payload)
req.Header.Add("x-api-key", "<api-key>")
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.patch("https://api.paj.cash/pub/v2/webhook")
.header("x-api-key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"rampWebhookURL\": \"https://example.com/webhook\",\n \"paymentWebhookURL\": \"https://example.com/payment-webhook\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.paj.cash/pub/v2/webhook")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Patch.new(url)
request["x-api-key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"rampWebhookURL\": \"https://example.com/webhook\",\n \"paymentWebhookURL\": \"https://example.com/payment-webhook\"\n}"
response = http.request(request)
puts response.read_body{
"rampWebhookURL": "https://example.com/webhook",
"paymentWebhookURL": "https://example.com/payment-webhook",
"webhookSecret": "whsec_2f1c…"
}Update your webhook URLs
Points the webhooks on the API key used for this request somewhere new. rampWebhookURL receives onramp and offramp order updates; paymentWebhookURL receives settled payments. Only the fields you send are changed, and sending one as an empty string clears it, stopping those deliveries. The full configuration after the change is returned.
curl --request PATCH \
--url https://api.paj.cash/pub/v2/webhook \
--header 'Content-Type: application/json' \
--header 'x-api-key: <api-key>' \
--data '
{
"rampWebhookURL": "https://example.com/webhook",
"paymentWebhookURL": "https://example.com/payment-webhook"
}
'import requests
url = "https://api.paj.cash/pub/v2/webhook"
payload = {
"rampWebhookURL": "https://example.com/webhook",
"paymentWebhookURL": "https://example.com/payment-webhook"
}
headers = {
"x-api-key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.patch(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'PATCH',
headers: {'x-api-key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
rampWebhookURL: 'https://example.com/webhook',
paymentWebhookURL: 'https://example.com/payment-webhook'
})
};
fetch('https://api.paj.cash/pub/v2/webhook', 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.paj.cash/pub/v2/webhook",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "PATCH",
CURLOPT_POSTFIELDS => json_encode([
'rampWebhookURL' => 'https://example.com/webhook',
'paymentWebhookURL' => 'https://example.com/payment-webhook'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"x-api-key: <api-key>"
],
]);
$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.paj.cash/pub/v2/webhook"
payload := strings.NewReader("{\n \"rampWebhookURL\": \"https://example.com/webhook\",\n \"paymentWebhookURL\": \"https://example.com/payment-webhook\"\n}")
req, _ := http.NewRequest("PATCH", url, payload)
req.Header.Add("x-api-key", "<api-key>")
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.patch("https://api.paj.cash/pub/v2/webhook")
.header("x-api-key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"rampWebhookURL\": \"https://example.com/webhook\",\n \"paymentWebhookURL\": \"https://example.com/payment-webhook\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.paj.cash/pub/v2/webhook")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Patch.new(url)
request["x-api-key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"rampWebhookURL\": \"https://example.com/webhook\",\n \"paymentWebhookURL\": \"https://example.com/payment-webhook\"\n}"
response = http.request(request)
puts response.read_body{
"rampWebhookURL": "https://example.com/webhook",
"paymentWebhookURL": "https://example.com/payment-webhook",
"webhookSecret": "whsec_2f1c…"
}x-api-key is the key that changes — there
is no id in the path, so a key can only ever reconfigure itself.
The two webhooks
rampWebhookURL— onramp and offramp order updates, posted at every status transition. It is the default for every order opened with this key: an order that sets its ownwebhookURLis announced there instead, and one that leaves it out is announced here.paymentWebhookURL— payment settlements.
Your signing secret
The response also carrieswebhookSecret — the secret every delivery to these
URLs is signed with, so your handler can tell a real delivery from anything else
that finds the URL. It is the same secret each time; changing a URL does not
change it.
Verifying is a few lines, and Webhook
signatures covers it in full. If you are moving a
webhook to a new endpoint, that is the moment to add verification to it.
Turning a webhook off
Send the field as an empty string:{ "paymentWebhookURL": "" }
400 and
nothing changes. A body naming neither field is also a 400 rather than a
silent no-op, since it usually means a misspelled field name.
After changing it
A newpaymentWebhookURL takes effect immediately, including for payments
already in flight — a payment opened a minute ago will settle to whatever
paymentWebhookURL says at the moment it settles, not what it said when it was
created.
rampWebhookURL works differently. An order that did not name its own webhook
takes the key’s rampWebhookURL when it is created, and keeps it. Changing
the URL covers every order you open from then on; orders already open keep
reporting to the URL they started with.
Worth pairing with Test your webhook: change the
URL, then fire a sample payload at it and confirm your new endpoint answers 2xx
before you rely on it.
One thing to check when moving a URL: redirects are not followed, so a delivery
to a URL that redirects — http to https, or a bare domain to www — counts
as failed. Point the webhook at the final destination.Authorizations
Body
Where onramp and offramp order updates are posted. Send an empty string to stop receiving them.
"https://example.com/webhook"
Where settled payments are posted. Send an empty string to stop receiving them.
"https://example.com/payment-webhook"
Response
The webhook configuration now on the key
Where onramp and offramp order updates are posted.
"https://example.com/webhook"
Where settled payments are posted.
"https://example.com/payment-webhook"
The secret every delivery to these URLs is signed with. Verify a delivery by computing HMAC-SHA256 over {X-PAJ-Timestamp}.{raw body} with this secret and comparing it, in constant time, against the hex digest in X-PAJ-Signature (after the v1= prefix). Reject deliveries whose timestamp is more than a few minutes old.
"whsec_2f1c…"