- Home
- API documentation
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 levelswrite:customers– create customers and change contact detailsread:appointments– read appointments and bookingsread: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 expired402 subscription_inactive– the school's PfotenCard subscription is not active403 plan_required– the module is not included in the booked plan403 module_disabled– the school has switched the module off403 insufficient_scope– permission missing400 invalid_parameter,404 not_found,409 already_exists429 rate_limited– more than 120 requests per minute and key
Endpoints
Base URL: https://api.pfotencard.de/api/v1
| Method | Path | Permission | Description |
|---|---|---|---|
| GET | /me | – | Check school and key (useful for testing the connection). |
| GET | /customers | read:customers | Customers incl. dogs. Filters: email, created_since. |
| GET | /customers/{id} | read:customers | Single customer. |
| POST | /customers | write:customers | Create customer (optionally with dogs and invitation email). |
| PATCH | /customers/{id} | write:customers | Change name, phone and address. |
| GET | /dogs | read:customers | Dogs. Filter: customer_id. |
| GET | /levels | read:customers | The school's levels/stages (for level_id). |
| GET | /appointments | read:appointments | Appointments in the period from/to (default: today + 90 days, max. 366 days). |
| GET | /appointments/{id} | read:appointments | Appointment with all bookings. |
| GET | /bookings | read:appointments | Bookings. Filters: customer_id, appointment_id, status, created_since. |
| GET | /transactions | read:transactions | Top-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.created | New customer (app, sign-up, import or API). |
| booking.created | New booking – also a waiting list place or re-booking after a cancellation. |
| booking.confirmed | Moved up from the waiting list. |
| booking.cancelled | Booking cancelled or deleted. |
| transaction.created | New transaction: top-up, charge for an appointment, cancellation, correction. |
| level.reached | Customer or dog has moved up a level. |
| ping | Only 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.