Pet Pro's APIv1
A REST API and outbound webhooks for connecting your own tools to your Pet Pro's account. Read your calendar, create bookings, sync customers, and receive events the moment they happen.
Overview
The base URL is https://petpros.com.au/api/v1. The /api/v1 surface is stable: we only make additive changes (new endpoints, new optional fields). Breaking changes would ship as a new version prefix.
API access and webhooks are included in the Bookings Pro plan. Keys belong to one business, so every request operates on your own account and there is no account parameter to pass.
Authentication
Create keys in your admin under Settings, then Developer. Each key is shown once at creation; we store only a hash. Send the key as a bearer header on every request:
curl https://petpros.com.au/api/v1/me \
-H "Authorization: Bearer ppr_live_your_key_here"
- Scopes:
read(GET endpoints) andread_write(adds the POST endpoints). A read-only key on a write endpoint gets403 insufficient_scope. - Keys are never accepted in the URL. A request with an
api_keyquery parameter is rejected so keys cannot leak into logs. - Treat keys like passwords. To rotate, create a new key, switch your integration over, then revoke the old one. Revocation is immediate.
Rate limits
- 300 requests per minute per key across all endpoints.
- 60 writes per minute per key on POST endpoints, counted on top of the request limit.
Every response carries X-RateLimit-Limit and X-RateLimit-Remaining. When you exceed a limit you get 429 rate_limited with a Retry-After header holding the seconds until the window resets.
Conventions
- JSON in and out, camelCase field names.
- Timestamps are ISO 8601 UTC (for example
2026-08-01T00:30:00.000Z). Responses that involve local times also include the business's IANAtimezone. - List endpoints paginate with
?page=N(1-based, 50 rows per page) and return{ "data": [...], "page": N, "hasMore": true|false }. - Request bodies are capped at 4 KB.
Errors
Errors use one envelope: { "error": "<code>", "message": "<human text>" }.
| HTTP | Code | Meaning |
|---|---|---|
| 401 | unauthorized | Missing, malformed, or revoked key. |
| 403 | plan_required | Your plan does not include API access. |
| 403 | insufficient_scope | Read-only key on a write endpoint. |
| 404 | not_found | No such resource in your account. |
| 400/422 | invalid_request / validation | Bad parameter or body. |
| 409 | slot_taken | The requested start time is not available. |
| 409 | invalid_state | The resource cannot make that transition (for example cancelling a completed booking). |
| 422 | idempotency_conflict | Idempotency-Key reused with a different body. |
| 429 | rate_limited | Rate limit hit; honour Retry-After. |
Endpoint reference
| Endpoint | Scope | Description |
|---|---|---|
GET/api/v1/me | read | Key introspection: business name, slug, timezone, currency, scopes. |
GET/api/v1/services | read | Active services with durations, prices and variants. |
GET/api/v1/staff | read | Active team members (id and name). |
GET/api/v1/availability | read | Bookable slots. Params: services (comma separated ids, required), from (YYYY-MM-DD), days (max 42), staff (any or an id), variants (serviceId:variantId pairs). |
GET/api/v1/appointments | read | List appointments. Filters: from, to, status, staffId, customerId, page. |
GET/api/v1/appointments/:id | read | One appointment with service, staff, customer and location. |
POST/api/v1/appointments | read_write | Create a confirmed booking in a real slot. Supports Idempotency-Key. |
POST/api/v1/appointments/:id/cancel | read_write | Cancel a confirmed appointment. Body: { "reason": "optional" }. |
GET/api/v1/customers | read | List customers. Params: q (matches name, email or phone), page. |
GET/api/v1/customers/:id | read | One customer with appointment stats. |
POST/api/v1/customers | read_write | Create or match a customer by email. 201 on create, 200 on match. |
GET/api/v1/reviews | read | Published first-party reviews for your business. |
Create a booking
Pick a slot from /api/v1/availability and post its exact start. Pass "staffId": "any" (or omit it) to let the engine assign a team member, exactly as the online booking page does.
curl -X POST https://petpros.com.au/api/v1/appointments \
-H "Authorization: Bearer ppr_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-12345" \
-d '{
"serviceId": 12,
"staffId": "any",
"start": "2026-08-01T00:30:00.000Z",
"customer": { "name": "Sam Taylor", "email": "sam@example.com" },
"notifyCustomer": true
}'
A successful create returns 201 with the full appointment. If the slot was taken in the meantime you get 409 slot_taken; fetch availability again and retry with a fresh slot. Set "notifyCustomer": false to suppress the confirmation email.
Not yet in v1 (planned): rescheduling via PATCH, sales and payment writes, managing webhook endpoints through the API, and OAuth apps. Webhook endpoints are managed in the admin for now.
Idempotency
POST /appointments and POST /customers accept an optional Idempotency-Key header (1 to 255 characters, any string unique to the operation). If a request is retried with the same key and the same body within 24 hours, the stored response is returned verbatim with the header Idempotency-Replayed: true, and no duplicate booking is created. The same key with a different body returns 422 idempotency_conflict. If the first request is still in flight you get 409 idempotency_in_progress; retry shortly.
Webhooks
Register up to 5 endpoints under Settings, then Developer. Each endpoint gets its own signing secret (whsec_...) and its own event subscription list. We POST a JSON body to your URL:
{
"event": "appointment.created",
"createdAt": "2026-08-01T00:30:05.123Z",
"data": {
"appointment": { "id": 91, "status": "confirmed", "startAtUtc": "..." },
"changeKind": "created"
}
}
Event types
| Event | Fires when |
|---|---|
appointment.created | A booking is confirmed on any channel: online, admin, mobile app, Reserve with Google, or the AI receptionist. |
appointment.updated | A booking changes state or moves: rescheduled, moved by the business, marked completed or no-show. data.changeKind carries the underlying change. |
appointment.cancelled | A booking is cancelled by the customer or the business. |
customer.created | A new customer record is created. |
review.created | A customer submits a review. |
sale.completed | A sale is completed at the register. |
Verifying signatures
Every delivery carries three headers: Petpros-Event (the event type), Petpros-Delivery-Id (unique per delivery), and Petpros-Signature in the form t=<unix seconds>,v1=<hex HMAC>. The signature is HMAC-SHA256 of <t>.<raw body> using your endpoint secret:
const crypto = require('node:crypto');
function verify(signatureHeader, rawBody, secret) {
const parts = Object.fromEntries(
signatureHeader.split(',').map((p) => p.split('=')),
);
const age = Math.abs(Date.now() / 1000 - Number(parts.t));
if (age > 300) return false; // reject anything older than 5 minutes
const expected = crypto.createHmac('sha256', secret)
.update(parts.t + '.' + rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected, 'hex'),
Buffer.from(parts.v1, 'hex'),
);
}
- Use a constant-time compare, as above.
- Reject deliveries whose timestamp is more than 5 minutes old.
- Delivery is at least once. Deduplicate on
Petpros-Delivery-Id: if our timeout fires after your server received the event, the redelivery reuses the same id. - Respond with a 2xx quickly and process the event asynchronously. We time out after 10 seconds.
Retries and disabling
A non-2xx response or a timeout is retried on a backoff of 1 minute, 5 minutes, 30 minutes, 2 hours, 12 hours, then 24 hours. After the final retry the delivery is marked dead; you can replay it from the delivery log in the admin. An endpoint that fails 50 deliveries in a row is disabled automatically and can be re-enabled from the same page once your receiver is fixed.
Changelog
- 20 July 2026: initial release of
/api/v1(me, services, staff, availability, appointments, customers, reviews) and webhooks (six event types, signed deliveries, retries, replay).