Skip to content

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.

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=S256

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

Terminal window
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.

Read the business your token acts for. This route needs the scheduling:read scope.

Terminal window
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 OK
Content-Type: application/json
X-RateLimit-Limit-App: 600
X-RateLimit-Remaining-App: 599
X-RateLimit-Limit-Install: 60
X-RateLimit-Remaining-Install: 59
X-RateLimit-Reset: 1755781260
// 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();
}
You gotMost likely causeFix
The consent page shows an error instead of coming backThe redirect URI is not registered exactly, or a scope your app did not declareFix the authorize URL; the page never redirects a request it cannot accept.
400 invalid_grant from the token endpointThe code expired (10 minutes), was used already, or redirect_uri / code_verifier do not matchSend the business through the consent page again.
401 …/errors/unauthorized from the APIToken expired or sent to the wrong hostRefresh; make sure you are calling api.unitedpetclub.com, not the token host.
401 0010010000402The business disconnected your appStop; ask the business to connect it again.
403 0010010000406A client-credentials token on a business routeUse the token from step 2.
403 0010010000407The business has no valid payment card on fileTell the business; it adds one in Dashboard → Finances → Billing.
403 …/errors/insufficient-scopeThe business did not grant the scope this route needsAsk for the scope on the consent page.
429Too many requestsHonour 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.