curl --request POST \
--url https://api.paj.cash/pub/v2/webhook/test \
--header 'x-api-key: <api-key>'import requests
url = "https://api.paj.cash/pub/v2/webhook/test"
headers = {"x-api-key": "<api-key>"}
response = requests.post(url, headers=headers)
print(response.text)const options = {method: 'POST', headers: {'x-api-key': '<api-key>'}};
fetch('https://api.paj.cash/pub/v2/webhook/test', 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/test",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"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"
"net/http"
"io"
)
func main() {
url := "https://api.paj.cash/pub/v2/webhook/test"
req, _ := http.NewRequest("POST", url, nil)
req.Header.Add("x-api-key", "<api-key>")
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.paj.cash/pub/v2/webhook/test")
.header("x-api-key", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.paj.cash/pub/v2/webhook/test")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["x-api-key"] = '<api-key>'
response = http.request(request)
puts response.read_body{
"url": "https://example.com/webhook",
"delivered": true,
"durationMs": 214,
"error": "Webhook call failed with status: 404"
}Test your webhook
Posts a sample payload to one of the webhooks configured on the API key used for this request, and reports whether it was accepted. type=RAMP, the default, posts an order update to rampWebhookURL; type=PAYMENT posts a settled payment to paymentWebhookURL. Either payload has the same shape as the real thing, so a webhook that handles this call handles production traffic. Nothing is created and no order or payment is affected. A webhook that rejects the call still returns 200 — the failure is described in the response body.
curl --request POST \
--url https://api.paj.cash/pub/v2/webhook/test \
--header 'x-api-key: <api-key>'import requests
url = "https://api.paj.cash/pub/v2/webhook/test"
headers = {"x-api-key": "<api-key>"}
response = requests.post(url, headers=headers)
print(response.text)const options = {method: 'POST', headers: {'x-api-key': '<api-key>'}};
fetch('https://api.paj.cash/pub/v2/webhook/test', 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/test",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"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"
"net/http"
"io"
)
func main() {
url := "https://api.paj.cash/pub/v2/webhook/test"
req, _ := http.NewRequest("POST", url, nil)
req.Header.Add("x-api-key", "<api-key>")
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.paj.cash/pub/v2/webhook/test")
.header("x-api-key", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.paj.cash/pub/v2/webhook/test")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["x-api-key"] = '<api-key>'
response = http.request(request)
puts response.read_body{
"url": "https://example.com/webhook",
"delivered": true,
"durationMs": 214,
"error": "Webhook call failed with status: 404"
}Choosing which webhook
An API key carries two, andtype picks between them:
RAMP(the default) — posts a sample order torampWebhookURL, the endpoint that receives onramp and offramp status updates.PAYMENT— posts a sample settled payment topaymentWebhookURL, the endpoint that receives payment settlements.
RAMP says nothing about whether
your payment handler works.
There is no request body. The URL is read from the key you authenticate with, so
whichever key you send in x-api-key is the one whose webhook gets called. That
is deliberate: it verifies the configuration Paj will actually use, not a URL you
retyped into a test form.
Configuring the URL
Both webhooks live on the API key, alongside its name, and are set from the dashboard when you create or edit a key. A key without one gets no pushes of that kind at all; asking to test a webhook the key has not configured is a400.
Reading the result
A rejected delivery is still a successful test, so a webhook that answers404
or times out comes back as a 200 with the detail in the body — you do not have
to unpack an error to find out what happened.
delivered—trueonly if your endpoint answered with a 2xx. Anything else, including a network failure, isfalse.error— present only whendeliveredisfalse. Either the rejecting status code or the transport error if the endpoint could not be reached.durationMs— how long the round trip took. Worth watching: a webhook that is slow here is slow in production, where a timeout is treated as a failed delivery.
What your endpoint receives
ForRAMP, a POST with an order payload in the same shape as the responses
from Create an onramp order and Create an
offramp order. The sample describes a
PROCESSING Solana offramp.
For PAYMENT, a POST with the payload from Create a
payment, with status at SUCCESSFUL — the
same thing a real settlement delivers. The sample describes a 25 USDC payment on
Solana.
Answer with any 2xx. The body is ignored — Paj reads it if there is one, but
nothing depends on its contents.
Either way the delivery is signed exactly as a real one is, with the same secret
and the same X-PAJ-Timestamp and X-PAJ-Signature headers. A verifier that
accepts this call accepts production traffic, which makes this the cheapest way
to confirm your signature checking works before a real settlement depends on it.
See Webhook signatures.
id does not refer to anything
real. Make sure a handler that looks records up by id fails softly on an
unknown one, or your test will report a failure that says more about the test
than about your webhook.Authorizations
Query Parameters
Which webhook to exercise. RAMP posts a sample order to rampWebhookURL; PAYMENT posts a sample settled payment to paymentWebhookURL.
RAMP, PAYMENT Response
The outcome of the delivery attempt
The webhook URL configured on the API key that was called
"https://example.com/webhook"
True if the endpoint answered with a 2xx
true
Round trip time of the delivery attempt, in milliseconds
214
Why the delivery failed — the rejecting status code, or the transport error if the endpoint could not be reached at all. Absent when delivered.
"Webhook call failed with status: 404"