Quickstart
Every business route of the API acts for one business that connected your app: its bookings, clients, animals,
sales and so on. So the first call needs a token the business gave you, from the authorization-code grant, carrying a scope your app
declared and the business granted on the consent page. A token your
app gets for itself with client credentials acts for no business, and the business routes refuse it with 403
0010010000406.
You need an approved application’s client_id and client_secret, a registered redirect URI (see
Authentication), and a test business that keeps a valid payment card on file.
1. Send the business to the consent page
Section titled “1. Send the business to the consent page”https://auth.unitedpetclub.com/connect/authorize ?response_type=code &client_id=YOUR_CLIENT_ID &redirect_uri=https%3A%2F%2Fapp.example.com%2Fupc%2Fcallback &scope=scheduling%3Aread &state=af0ifjsldkj &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM &code_challenge_method=S256The business signs in, sees your app and the scopes you ask for, and chooses Allow. The browser comes back to your
redirect URI with ?code=upcac_...&state=af0ifjsldkj. Check state first. Details, including PKCE, are in
Connecting a business.
2. Exchange the code for tokens
Section titled “2. Exchange the code for tokens”curl -s -X POST https://auth.unitedpetclub.com/connect/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "code=upcac_..." \ -d "redirect_uri=https://app.example.com/upc/callback" \ -d "code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" \ -d "client_id=$UPC_CLIENT_ID" \ -d "client_secret=$UPC_CLIENT_SECRET"{ "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6ImRldi1hY2Nlc3MifQ...", "token_type": "Bearer", "expires_in": 3600, "scope": "scheduling:read", "refresh_token": "upcrt_..."}Keep access_token in memory for the next expires_in seconds. Store refresh_token with the business, encrypted:
refresh tokens rotate, so always keep the newest.
3. Call the API
Section titled “3. Call the API”Read the business your token acts for. This route needs the scheduling:read scope.
curl -s https://api.unitedpetclub.com/api/scheduling/v1/business \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Accept: application/json"There is no business id in the path: the token names the business. A 200 answers its businessProfileId,
timezoneIanaName and isAcceptingBookings, and the headers on every response tell you where you stand against
your rate limits:
HTTP/1.1 200 OKContent-Type: application/jsonX-RateLimit-Limit-App: 600X-RateLimit-Remaining-App: 599X-RateLimit-Limit-Install: 60X-RateLimit-Remaining-Install: 59X-RateLimit-Reset: 1755781260The same thing in code
Section titled “The same thing in code”// Node 18+ — no dependencies. One connected business; load and save its refresh token from your store.const TOKEN_URL = "https://auth.unitedpetclub.com/connect/token";const API = "https://api.unitedpetclub.com";
let cached = null; // { token, expiresAt }
async function getToken(business) { if (cached && Date.now() < cached.expiresAt - 60_000) return cached.token; const res = await fetch(TOKEN_URL, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: business.refreshToken, client_id: process.env.UPC_CLIENT_ID, client_secret: process.env.UPC_CLIENT_SECRET, }), }); if (!res.ok) throw new Error(`token request failed: ${res.status} ${await res.text()}`); const body = await res.json(); business.refreshToken = body.refresh_token; // rotated: save it before you use the access token await business.save(); cached = { token: body.access_token, expiresAt: Date.now() + body.expires_in * 1000 }; return cached.token;}
async function getBusiness(business, retried = false) { const res = await fetch(`${API}/api/scheduling/v1/business`, { headers: { Authorization: `Bearer ${await getToken(business)}`, Accept: "application/json" }, }); if (res.status === 401 && !retried) { // expired: refresh once. A 401 0010010000402 means the business disconnected. cached = null; return getBusiness(business, true); } if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`); return res.json();}If something went wrong
Section titled “If something went wrong”| You got | Most likely cause | Fix |
|---|---|---|
| The consent page shows an error instead of coming back | The redirect URI is not registered exactly, or a scope your app did not declare | Fix the authorize URL; the page never redirects a request it cannot accept. |
400 invalid_grant from the token endpoint | The code expired (10 minutes), was used already, or redirect_uri / code_verifier do not match | Send the business through the consent page again. |
401 …/errors/unauthorized from the API | Token expired or sent to the wrong host | Refresh; make sure you are calling api.unitedpetclub.com, not the token host. |
401 0010010000402 | The business disconnected your app | Stop; ask the business to connect it again. |
403 0010010000406 | A client-credentials token on a business route | Use the token from step 2. |
403 0010010000407 | The business has no valid payment card on file | Tell the business; it adds one in Dashboard → Finances → Billing. |
403 …/errors/insufficient-scope | The business did not grant the scope this route needs | Ask for the scope on the consent page. |
429 | Too many requests | Honour Retry-After — see Rate limits. |
The same token works on the business resources (animals, sales,
invoices, messages and more) once your app declares their scopes and the business grants them: ask for animal:read
alongside scheduling:read, for example, and call GET /api/animal/v1/animals. Those scopes are on the development
environment only for now (2026-10-18), and a self-registered app gets the booking scopes only.
- Authentication — the full token contract and scope list.
- The Partner API — services, slots, bookings, clients and webhooks.
- Pagination and Errors before you build the loop.
- API reference for every endpoint.