PawDNS REST API v1: Getting started
Base URL: https://sandbox.pawdns.de/api/v1.
The full contract is the OpenAPI-Spec
(also as JSON).
1. Get a sandbox token
Ask PawHost staff for a sandbox token. Admins create one under "API sandbox" in the PawDNS admin. Sandbox tokens start with
pdns_test_.
- A sandbox token uses the same URL as production, but it only sees the sandbox tenants: "Sandbox Reseller" with the customers
whmcs:1001(Example GmbH) andwhmcs:1002(Muster AG). - Its zones live on a fake backend. Nothing reaches real nameservers.
- Publication still moves through
pending → pushed → live(within a few seconds). - The data is reset every night. Tokens survive the reset.
Production tokens start with pdns_live_.
Keep every token on your server and never ship it to a browser.
2. First requests
export BASE=https://sandbox.pawdns.de/api/v1
export T=pdns_test_… # your token
# Who am I? (tenant, limits, features, scopes)
curl -s -H "Authorization: Bearer $T" -H "Accept-Language: en" "$BASE/me"
# Zones (cursor pagination: pass pagination.next_cursor as ?cursor=)
curl -s -H "Authorization: Bearer $T" "$BASE/zones?limit=50"
3. Acting for a customer
A panel token (scope act_as) sends the customer with every request. PawDNS then answers exactly as if the customer had made the request: their zones, IPs, quotas and features. The header only says who acts. PawDNS looks up what they own itself.
curl -s -H "Authorization: Bearer $T" -H "X-Act-As-Customer: whmcs:1001" "$BASE/me"
curl -s -H "Authorization: Bearer $T" -H "X-Act-As-Customer: ten_4821" "$BASE/zones"
Customers outside your token's tenant tree give 403 act_as_not_allowed,
unknown ones give 404 act_as_unknown.
4. Writes, idempotency and concurrency
curl -s -H "Authorization: Bearer $T" -H "X-Act-As-Customer: whmcs:1001" \
-H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
-d '{"comment": "new web server", "changes": [
{"op": "replace_rrset", "name": "www", "type": "A", "ttl": 3600, "records": [{"address": "192.0.2.20"}]}
]}' \
"$BASE/zones/example.com/changesets"
- Idempotency-Key (every POST/PATCH):
retry network errors, 429 and 5xx with the same key. PawDNS replays the first response (header Idempotent-Replayed: true). The same key with a different body gives
422 idempotency_key_reused. Keys are kept for 24 h. - ETag / If-Match: send the ETag of a zone or record as If-Match on PATCH/DELETE. If someone changed the object in the meantime, the answer is
412 precondition_failed. - Errors:
application/problem+json. Branch oncode(stable), never on the text. title and detail follow Accept-Language (de or en). Quote request_id (also in the X-Request-Id header) in support requests. - Rate limits: watch the RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset headers. On 429, wait for Retry-After.
5. Webhooks: verifying the signature
Every delivery carries PawDns-Signature: t=<unix>,v1=<hex>
with v1 = hex(HMAC-SHA256(secret, "<t>.<raw body>")).
- Compare in constant time.
- Reject deliveries whose t is more than 5 minutes off.
- Accept any of the v1 values: during a secret rotation there are two for 24 h.
- Deduplicate on PawDns-Delivery.
// PHP
function pawdnsVerify(string $rawBody, string $header, string $secret): bool {
$parts = [];
foreach (explode(',', $header) as $kv) { [$k, $v] = array_pad(explode('=', trim($kv), 2), 2, ''); $parts[$k][] = $v; }
$t = (int) ($parts['t'][0] ?? 0);
if (abs(time() - $t) > 300) return false;
$expected = hash_hmac('sha256', $t.'.'.$rawBody, $secret);
foreach ($parts['v1'] ?? [] as $sig) { if (hash_equals($expected, $sig)) return true; }
return false;
}
// $ok = pawdnsVerify(file_get_contents('php://input'), $_SERVER['HTTP_PAWDNS_SIGNATURE'] ?? '', $secret);
// Node.js
const crypto = require('crypto');
function pawdnsVerify(rawBody, header, secret) {
const parts = header.split(',').map(s => s.trim().split('='));
const t = Number((parts.find(([k]) => k === 't') || [])[1]);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
return parts.filter(([k]) => k === 'v1').some(([, v]) =>
v.length === expected.length && crypto.timingSafeEqual(Buffer.from(v), Buffer.from(expected)));
}
# Python
import hmac, hashlib, time
def pawdns_verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = [p.strip().split("=", 1) for p in header.split(",")]
t = int(next((v for k, v in parts if k == "t"), "0"))
if abs(time.time() - t) > 300:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(expected, v) for k, v in parts if k == "v1")
Answer with any 2xx within 10 s. Failed deliveries are retried after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h.
6. Next steps
- Swagger UI: click "Authorize", paste your pdns_test_ token, keep the Sandbox server and use "Try it out" on any operation.
- Mock server without any token:
npx @stoplight/prism-cli mock https://sandbox.pawdns.de/api/docs/openapi.yaml