PawDNS REST APIGetting started
← API reference (Swagger UI) English · Deutsch

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_.

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"

5. Webhooks: verifying the signature

Every delivery carries PawDns-Signature: t=<unix>,v1=<hex> with v1 = hex(HMAC-SHA256(secret, "<t>.<raw body>")).

// 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