For developers

PfotenCard API & Webhooks

Connect your dog school software with your website, newsletter tool, accounting or automations such as Zapier, Make and n8n. The REST API speaks JSON, webhooks report events in real time and are signed.

  • Version v1, stable data formats
  • Access via API key with individually selectable permissions
  • Webhooks with HMAC-SHA256 signature and automatic retries
  • Available with the “API & Webhooks” module (depending on your package or as an add-on)

Quick start

1. Activate the module in the settings under App modules → API & Webhooks (admin access required). 2. Create an API key with the required permissions – it is shown exactly once. 3. Send the key in the header with every request:

curl https://api.pfotencard.de/api/v1/me \
  -H "Authorization: Bearer pc_live_…"

The dog school is determined by the key – you don't need to specify a school ID. Keys belong on a server or in an automation tool, never in public website code. The API is intended for server-to-server calls; browser calls from other domains are not permitted.

Basics

Permissions (scopes)

  • read:customers – read customers, dogs and levels
  • write:customers – create customers and change contact details
  • read:appointments – read appointments and bookings
  • read:transactions – read transactions

Pagination

Lists are sorted by ID in ascending order and return data, has_more and next_cursor. For the next page, append starting_after=<next_cursor>. limit: 1–100 (appointments, bookings, transactions up to 500).

GET https://api.pfotencard.de/api/v1/customers?limit=50&starting_after=1234

{
  "data": [ { "id": 1240, "first_name": "Kira", ... } ],
  "has_more": true,
  "next_cursor": "1289"
}

Error

Errors always come in the same format with the appropriate HTTP status:

HTTP/1.1 403 Forbidden
{ "error": { "code": "insufficient_scope", "message": "Dem API-Key fehlt die Berechtigung \"read:transactions\"." } }
  • 401 unauthorized / key_expired – key is missing, wrong, revoked or expired
  • 402 subscription_inactive – the school's PfotenCard subscription is not active
  • 403 plan_required – the module is not included in the booked plan
  • 403 module_disabled – the school has switched the module off
  • 403 insufficient_scope – permission missing
  • 400 invalid_parameter, 404 not_found, 409 already_exists
  • 429 rate_limited – more than 120 requests per minute and key

Endpoints

Base URL: https://api.pfotencard.de/api/v1

MethodPathPermissionDescription
GET/me–Check school and key (useful for testing the connection).
GET/customersread:customersCustomers incl. dogs. Filters: email, created_since.
GET/customers/{id}read:customersSingle customer.
POST/customerswrite:customersCreate customer (optionally with dogs and invitation email).
PATCH/customers/{id}write:customersChange name, phone and address.
GET/dogsread:customersDogs. Filter: customer_id.
GET/levelsread:customersThe school's levels/stages (for level_id).
GET/appointmentsread:appointmentsAppointments in the period from/to (default: today + 90 days, max. 366 days).
GET/appointments/{id}read:appointmentsAppointment with all bookings.
GET/bookingsread:appointmentsBookings. Filters: customer_id, appointment_id, status, created_since.
GET/transactionsread:transactionsTop-ups, charges, cancellations. Filters: customer_id, from, to.

Create customer

email and a name are required. With send_invite: true the customer receives the school's usual invitation email to set their password. If the email already exists, the API responds with 409 and the existing customer_id.

curl -X POST https://api.pfotencard.de/api/v1/customers \
  -H "Authorization: Bearer pc_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "nora@example.de",
    "first_name": "Nora",
    "last_name": "Neumann",
    "phone": "0171 1234567",
    "address": { "street": "Hauptstr. 1", "postcode": "50667", "city": "Köln", "country": "DE" },
    "dogs": [ { "name": "Fips", "breed": "Mops", "birth_date": "2024-05-01" } ],
    "send_invite": true
  }'

Customer (response format)

{
  "id": 1240,
  "first_name": "Nora",
  "last_name": "Neumann",
  "name": "Nora Neumann",
  "email": "nora@example.de",
  "phone": "0171 1234567",
  "address": { "street": "Hauptstr. 1", "postcode": "50667", "city": "Köln", "country": "DE" },
  "balance": 0,
  "level_id": 12,
  "is_active": true,
  "customer_since": "2026-09-24T09:12:00.000Z",
  "dogs": [ { "id": 881, "name": "Fips", "breed": "Mops", "birth_date": "2024-05-01", "level_id": null } ]
}

Email address (login), credit and level deliberately cannot be changed via the API – this is reserved for the app so that accounting and level rules stay correct. Write calls appear in the school's change log with the source “API”.

Webhooks

You set up webhooks in the settings: enter an https address, choose events, done. PfotenCard then sends a POST with JSON to this address for each event – usually a few seconds after the action.

customer.createdNew customer (app, sign-up, import or API).
booking.createdNew booking – also a waiting list place or re-booking after a cancellation.
booking.confirmedMoved up from the waiting list.
booking.cancelledBooking cancelled or deleted.
transaction.createdNew transaction: top-up, charge for an appointment, cancellation, correction.
level.reachedCustomer or dog has moved up a level.
pingOnly when using the “Send test” button.

Structure of a message

POST /dein-webhook HTTP/1.1
Content-Type: application/json
PfotenCard-Event: booking.created
PfotenCard-Delivery: 5821
PfotenCard-Signature: t=1790241234,v1=5f2b…c9

{
  "id": "evt_10422",
  "type": "booking.created",
  "created_at": "2026-09-24T09:13:54.000Z",
  "school": "deine-hundeschule",
  "data": {
    "id": 7788, "appointment_id": 912, "customer_id": 1240, "dog_id": 881, "dog_name": "Fips",
    "status": "confirmed", "attended": false, "billed": false,
    "appointment": { "id": 912, "title": "Welpengruppe", "start": "2026-09-26T08:00:00.000Z", "end": "2026-09-26T09:00:00.000Z" }
  }
}

data has the same format as the respective API resource and shows the state at the time of delivery. If a record has been deleted by then, data contains the state at the time of the event and "deleted": true. Use the event's id to detect duplicate deliveries – in rare cases a message may arrive more than once.

Verify signature

The PfotenCard-Signature header contains a timestamp t and v1 = HMAC-SHA256(secret, t + "." + raw body) as a hex value. Verify using the unmodified body and reject messages whose timestamp is older than about five minutes.

// Node.js / Express
import crypto from 'node:crypto';
import express from 'express';

const app = express();
const SECRET = process.env.PFOTENCARD_WEBHOOK_SECRET; // whsec_…

app.post('/pfotencard-webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const header = req.get('PfotenCard-Signature') || '';
  const t = /t=(\d+)/.exec(header)?.[1];
  const v1 = /v1=([0-9a-f]{64})/.exec(header)?.[1];
  if (!t || !v1 || Math.abs(Date.now() / 1000 - Number(t)) > 300) return res.sendStatus(400);

  const expected = crypto.createHmac('sha256', SECRET).update(`${t}.${req.body}`).digest('hex');
  if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1))) return res.sendStatus(400);

  const event = JSON.parse(req.body.toString('utf8'));
  // … verarbeiten (idempotent anhand event.id)
  res.sendStatus(200);
});
# PHP
$payload = file_get_contents('php://input');
preg_match('/t=(\d+),v1=([0-9a-f]{64})/', $_SERVER['HTTP_PFOTENCARD_SIGNATURE'] ?? '', $m);
$expected = hash_hmac('sha256', $m[1] . '.' . $payload, getenv('PFOTENCARD_WEBHOOK_SECRET'));
if (!$m || !hash_equals($expected, $m[2]) || abs(time() - (int)$m[1]) > 300) { http_response_code(400); exit; }
$event = json_decode($payload, true);

Response, retries, pausing

Respond within 8 seconds with a 2xx status. Redirects are not followed. If a delivery fails, PfotenCard retries after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 24 hours. Every attempt is listed in the log in the settings and can be triggered again there. If ten events in a row fail to arrive, the webhook is paused and the school's admins are notified. Logs are kept for 30 days.

For security reasons only public https addresses are allowed – no internal networks, no localhost.

Plan and data protection

API & Webhooks are included depending on your PfotenCard package or can be booked as an add-on. This is checked with every request: if the plan or subscription ends, keys and webhooks are paused automatically and continue to work without being set up again once you book again.

Personal data of customers flows to the connected program via the API. The dog school is responsible for ensuring that it is used in compliance with data protection law (e.g. a data processing agreement with the provider). Keys can be revoked individually at any time.

No PfotenCard yet?

Customers, dogs, appointments and credit in one place – and open to your other tools with the API. Apply for beta access.