Just A Good Guy Docs
Sign in

Developers

REST API

Read and create bookings, check availability and look up customers from your own server, with a secret key.

For developers
On this page

Overview

Base URLhttps://justagoodguy.us/api/v1
AuthAuthorization: Bearer sk_...
FormatJSON in, JSON out. Send Content-Type: application/json with every body.
TimesISO 8601. Responses use UTC, like 2026-11-03T19:00:00.000Z.
Rate limit600 requests per minute per key
Machine-readable spec/api/openapi.json, browsable in the API reference

The OpenAPI document is generated from the same schemas the server uses to check requests, so it always matches what the server accepts.

Authentication

Create a secret key in More → Developers (only the business owner can). It starts with sk_ and is shown once, so copy it somewhere safe. We only keep a fingerprint of it.

Shell
curl https://justagoodguy.us/api/v1/services \
  -H "Authorization: Bearer sk_your_secret_key"
  • Secret keys can read and change your bookings and customers. Keep them on your server. Never put one in a web page or an app.
  • For the browser, use your publishable key (pk_...) with the widget or the public API instead.
  • Revoke a key in More → Developers. It stops working straight away. The dashboard also shows when each key was last used.

Errors

Errors always come back as JSON with a plain-English message and a real HTTP status:

JSON
{ "error": "Sorry, that time was just taken or isn’t available. Please pick another time." }
StatusMeaning
400Something in the request is missing or wrong. The message says what, for example Please check the “service_id” field.
401No key, not a secret key, or a revoked key
404That booking, customer or service doesn't exist in your business
409The time isn't available (taken, outside hours, too soon or too far ahead)
429Over the rate limit. Wait for the Retry-After header (60 seconds) and try again.
500Something broke on our side. It's safe to retry reads; check before retrying a booking.

The messages are written for people, so you can show them to your users as they are.

Rate limits

Each secret key can make 600 requests per minute. The count resets every minute. Requests over the limit get 429 with { "error": "Rate limit: 600 requests per minute." } and a Retry-After: 60 header.

Booking statuses

statusMeaning
pending_approvalWaiting for the business to approve it
confirmedBooked
completedThe business marked it done
cancelledCancelled by the customer, the business or the API
no_showThe customer didn't turn up

source says where the booking came from: web_chat, widget, booking_page, phone, sms, dashboard, assistant or api.

The Booking object

JSON
{
  "id": "bkg_8f3k2m9q1x7w4r6t",
  "status": "confirmed",
  "source": "api",
  "start": "2026-11-03T19:00:00.000Z",
  "end": "2026-11-03T19:45:00.000Z",
  "timezone": "America/New_York",
  "service": { "id": "svc_2n4p6r8t0v1x3z5b", "name": "Haircut" },
  "staff": { "id": "stf_9h7f5d3b1z0x8v6t", "name": "Dana" },
  "customer": { "id": "cus_4k6m8p0r2t4v6x8z", "name": "Sam Lee", "phone": "+15555550123", "email": null },
  "priceCents": 4500,
  "notes": null
}

staff is null when no person is attached. Money is in cents of the business's currency. priceCents is null when the service has no set price.

Services and team

List services

GET /v1/services returns your active services.

Shell
curl https://justagoodguy.us/api/v1/services -H "Authorization: Bearer $JAGG_KEY"
JSON
{
  "data": [
    { "id": "svc_2n4p6r8t0v1x3z5b", "name": "Haircut", "kind": "appointment", "durationMin": 45, "priceCents": 4500, "description": null }
  ]
}

kind is appointment, visit, class or quote. See service kinds.

List team members

GET /v1/staff returns your active team members.

Shell
curl https://justagoodguy.us/api/v1/staff -H "Authorization: Bearer $JAGG_KEY"
JSON
{ "data": [{ "id": "stf_9h7f5d3b1z0x8v6t", "name": "Dana", "color": "#2563eb" }] }

Availability

Open times for a service

GET /v1/availability

QueryRequiredNotes
service_idyes
staff_idnoOnly this person's times. Without it, a time is open if anyone who does the service is free.
fromnoISO date-time to start from. Defaults to now. Times in the past are never returned.
daysnoHow many days to look ahead, 1 to 31. Default 7.
Shell
curl "https://justagoodguy.us/api/v1/availability?service_id=svc_2n4p6r8t0v1x3z5b&days=3" \
  -H "Authorization: Bearer $JAGG_KEY"
JSON
{
  "timezone": "America/New_York",
  "data": [
    {
      "date": "2026-11-03",
      "label": "Tuesday, Nov 3",
      "slots": [
        { "start": "2026-11-03T19:00:00.000Z", "label": "2:00 PM", "staffIds": ["stf_9h7f5d3b1z0x8v6t"] }
      ]
    }
  ]
}

Days are grouped by the business's local date, and label is ready to show. Days with no open times are left out. The business's minimum notice and how-far-ahead rules apply, and quote-type services never have open times.

Bookings

List bookings

GET /v1/bookings

QueryNotes
fromISO date-time. Default: 24 hours ago.
toISO date-time. Default: 30 days after from.
statusComma-separated statuses, for example confirmed,pending_approval. Default: all.

Returns bookings that overlap the range, oldest first.

Shell
curl "https://justagoodguy.us/api/v1/bookings?from=2026-11-01T00:00:00Z&to=2026-11-08T00:00:00Z&status=confirmed" \
  -H "Authorization: Bearer $JAGG_KEY"
JSON
{ "data": [ { "id": "bkg_8f3k2m9q1x7w4r6t", "status": "confirmed", "...": "a Booking object" } ] }

Create a booking

POST /v1/bookings

FieldRequiredNotes
service_idyes
startyesAn exact start from availability. Send it back as you got it.
staff_idnoBook with this person. Without it, the booking goes to whoever is free with the fewest bookings that day.
customer.nameyes
customer.phone or customer.emailone of themUsed to match an existing customer and to send the confirmation
notesnoUp to 1,000 characters, shown to the business
Shell
curl https://justagoodguy.us/api/v1/bookings \
  -H "Authorization: Bearer $JAGG_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "service_id": "svc_2n4p6r8t0v1x3z5b",
    "start": "2026-11-03T19:00:00.000Z",
    "customer": { "name": "Sam Lee", "phone": "+15555550123" },
    "notes": "Booked through our members portal"
  }'

Returns 201 with the Booking.

What happens next is the same as for any other booking:

  • The customer is matched by phone or email, or added as a new customer.
  • They get a confirmation by text and email (or "Request received" if the business approves each booking).
  • The business gets a "New booking" alert, and the booking.created webhook fires.
  • If the service needs approval, status is pending_approval.

There are no idempotency keys. If a create request times out, list bookings for that time before retrying. Retrying a booking that did go through usually fails with 409, because the time is now taken.

Get a booking

GET /v1/bookings/{id}

Shell
curl https://justagoodguy.us/api/v1/bookings/bkg_8f3k2m9q1x7w4r6t -H "Authorization: Bearer $JAGG_KEY"

Move a booking

POST /v1/bookings/{id}/reschedule

FieldRequiredNotes
startyesAn exact open start from availability
staff_idnoMove to this person. Without it, the same person is kept if they're free, otherwise anyone free.
notifynoText and email the customer about the change. Default true.
Shell
curl https://justagoodguy.us/api/v1/bookings/bkg_8f3k2m9q1x7w4r6t/reschedule \
  -H "Authorization: Bearer $JAGG_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "start": "2026-11-04T15:30:00.000Z" }'

Only pending_approval and confirmed bookings can be moved (others get 400). Reminders are rescheduled for the new time, and the booking.rescheduled webhook fires.

Cancel a booking

POST /v1/bookings/{id}/cancel

FieldRequiredNotes
reasonnoSaved on the booking
notifynoTell the customer. Default true.
Shell
curl https://justagoodguy.us/api/v1/bookings/bkg_8f3k2m9q1x7w4r6t/cancel \
  -H "Authorization: Bearer $JAGG_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "Customer called to cancel" }'

Cancelling an already-cancelled booking returns it unchanged. Pending reminders are stopped and the booking.cancelled webhook fires.

Customers

The Customer object

JSON
{ "id": "cus_4k6m8p0r2t4v6x8z", "name": "Sam Lee", "phone": "+15555550123", "email": null, "visitCount": 3, "tags": ["regular"] }

List or search customers

GET /v1/customers

QueryNotes
qPart of a name, for example sam
limit1 to 500. Default 100.

Most recently updated first.

Shell
curl "https://justagoodguy.us/api/v1/customers?q=sam&limit=20" -H "Authorization: Bearer $JAGG_KEY"

Create or update a customer

POST /v1/customers with name and a phone or email. If a customer with that phone or email exists, it's updated instead of duplicated:

  • a missing phone or email is filled in
  • an existing name is kept (a different spelling is saved as an alias)
Shell
curl https://justagoodguy.us/api/v1/customers \
  -H "Authorization: Bearer $JAGG_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Sam Lee", "phone": "+15555550123", "email": "[email protected]" }'

Returns 201 with the Customer. Phone numbers are stored in international format (+1...); a 10-digit number is read as a US or Canadian number.

Get a customer

GET /v1/customers/{id}

Shell
curl https://justagoodguy.us/api/v1/customers/cus_4k6m8p0r2t4v6x8z -H "Authorization: Bearer $JAGG_KEY"

Requests

Open call-backs

GET /v1/callbacks returns call-back requests that are still open.

Shell
curl https://justagoodguy.us/api/v1/callbacks -H "Authorization: Bearer $JAGG_KEY"
JSON
{
  "data": [
    { "id": "cbk_3j5l7n9q1s3u5w7y", "customerId": "cus_4k6m8p0r2t4v6x8z", "windowStart": "2026-11-03T21:00:00.000Z", "windowEnd": "2026-11-03T23:00:00.000Z", "reason": "Quote for a new water heater", "status": "open" }
  ]
}

Messages and quote requests

GET /v1/leads returns messages and quote requests with status new or contacted.

Shell
curl https://justagoodguy.us/api/v1/leads -H "Authorization: Bearer $JAGG_KEY"
JSON
{
  "data": [
    { "id": "led_6c8e0g2j4l6n8q0s", "customerId": "cus_4k6m8p0r2t4v6x8z", "kind": "quote", "details": "Leaking tap in the kitchen", "status": "new", "createdAt": "2026-11-02T14:12:00.000Z" }
  ]
}

kind is quote (taken by the receptionist) or message (sent from a contact form).

A complete example

Find the next open time for a service and book it, in Node 18 or later:

JavaScript
const API = 'https://justagoodguy.us/api/v1';
const headers = { Authorization: `Bearer ${process.env.JAGG_KEY}`, 'Content-Type': 'application/json' };

async function call(path, init) {
  const res = await fetch(API + path, { headers, ...init });
  const body = await res.json();
  if (!res.ok) throw new Error(`${res.status}: ${body.error}`);
  return body;
}

const { data: services } = await call('/services');
const haircut = services.find((s) => s.name === 'Haircut');

const { data: days } = await call(`/availability?service_id=${haircut.id}&days=7`);
const first = days[0]?.slots[0];
if (!first) throw new Error('Nothing open this week');

const booking = await call('/bookings', {
  method: 'POST',
  body: JSON.stringify({ service_id: haircut.id, start: first.start, customer: { name: 'Sam Lee', phone: '+15555550123' } }),
});
console.log(booking.status, booking.start);

Search the docs