Skip to content

The SMG Tools API

Read and write a business’s clients, jobs, leads and appointments, read its invoices, and subscribe to what happens, from any program the business gives a key.

Every plan Version 1

Get started

A key is made by the business, not by you. Its owner opens Integrations → API keys in SMG Tools, chooses New key, names it after the program it is for and ticks what it may do. The key is shown once; they copy it to you or paste it into the program. Every plan can issue keys.

Send the key with every request as a bearer token. The API answers at https://smg-api.smgtools.com/v1, in JSON:

curl 'https://smg-api.smgtools.com/v1/me' \
  -H 'Authorization: Bearer smgk_…'

That call works for any key and says which business it belongs to, the key’s name and what it may do — a good first request, and the one a program makes to show which account it is connected to.

A key sees the whole business, as a manager does, and does only what its scopes allow. What it writes goes through the same rules as the business’s own work, with the same effects, and the record says the key did it: a client added through the API says Zapier (API key) added it, and a job it completes runs the business’s automations for a completed job just as the office completing it would. A key never charges, refunds or deletes, and never sends an invoice, a proposal or a message of its own.

What a key may do

Each route needs one scope, and a key has the ones the business ticked when it made the key. A request for anything else is refused with scope_required, naming the scope it needed.

Any key may call GET /v1/me and GET /v1/webhooks/events, whatever it was allowed.

Errors

Every error is application/problem+json in one shape: the HTTP status, a stable code to branch on, a title a person can read, the scope a key lacked, and errors — each field’s problems — when a write was refused. A code is never renamed; new ones may be added, so treat one you do not know by its status.

{
  "status": 422,
  "code": "validation_failed",
  "title": "En Route and On Site can only be reported by a worker assigned to the job, from the field view",
  "errors": {
    "status": ["En Route and On Site can only be reported by a worker assigned to the job, from the field view"]
  }
}
CodeStatusMeans
invalid_key 401 No key was sent, or it is not one any business issued. Keys begin smgk_.
key_revoked 401 The business revoked the key. Ask them for a new one.
scope_required 403 The key was not allowed this when it was issued. The problem’s scope names the one it lacks.
not_found 404 Nothing at this address in the business — a record it does not have, or one this key may not see.
validation_failed 422 A rule refused the request. The title says why in a sentence, and errors names each field’s problems, all at once.
checklist_outstanding 422 A job cannot be completed while a required checklist step is unanswered, unless checklistOverrideReason says why.
idempotency_conflict 409 The Idempotency-Key was sent before with a different request, or its first request is still being answered.
rate_limited 429 Too many requests. Wait the seconds in the Retry-After header, then try again.
invalid_request 400 The request could not be read: a body that is not JSON, a parameter of the wrong kind, an Idempotency-Key over 255 characters.
method_not_allowed 405 The address does not take that method.
internal_error 500 Something went wrong on our side. A create sent with an Idempotency-Key is safe to retry.

Lists, paging and syncing

Every list answers a page at a time: data, with page, pageSize and total. Ask for a page with page (from 1) and pageSize (up to 100). Lists are oldest first, so a page you have read does not change under you as records are added.

Every list takes updatedSince, an instant: only the records created or changed at or after it are returned. A program that keeps a copy in step asks for what changed since its last sync, rather than reading everything again. Times are UTC instants; a date is the business’s own calendar day.

Retrying a create

Send an Idempotency-Key header — any value up to 255 characters, new for each thing you mean to create — with every create. If the answer is lost and you send the same request with the same key again within 24 hours, you are given the first answer rather than a second record, with the header Idempotent-Replayed: true. The same key with a different request is refused as idempotency_conflict.

Limits

Each key may make 120 requests a minute, and a business 600 a minute across all its keys. A request past either is refused with rate_limited and a Retry-After header in seconds. A business may have twenty working keys.

Checking a webhook came from us

A key allowed webhooks:write can subscribe an address to events — see Webhooks below. Each delivery is a POST of JSON with the headers X-SmgTools-Event, X-SmgTools-Delivery-Id, X-SmgTools-Timestamp and X-SmgTools-Signature. The signature reads t={timestamp},v1={signature}: an HMAC-SHA256, in lowercase hex, of the timestamp, a period and the raw body, keyed with the subscription’s secret exactly as it was given to you, whsec_ and all.

// Node.js
const [t, v1] = header.split(',').map((part) => part.split('=')[1]);
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
const genuine = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));

Compare against the body as it arrived, before parsing it, and refuse a timestamp more than a few minutes old so a delivery cannot be replayed. The secret is in the answer that created the subscription and never again.

Versions and the contract

This is version 1. Within it we only add — a new route, a new field, a new error code — so a program should ignore a field it does not know. Anything that would break a program comes in a new version, with notice before the old one is retired.

The API describes itself in OpenAPI 3.1 at https://smg-api.smgtools.com/v1/openapi.json, for any tool that generates a client from it. The reference below is written from the same document.

Me

Which business and which key is calling, and what the key may do

GET /v1/me any key

Answers 200 with Me.

curl -X GET 'https://smg-api.smgtools.com/v1/me' \
  -H 'Authorization: Bearer smgk_…'

The Me record

FieldTypeNotes
business Business
key Key

The Business record

FieldTypeNotes
id string (uuid)
name string

The Key record

FieldTypeNotes
id string (uuid)
name string
scopes array of string

Clients

The business's clients, a page at a time, oldest first

GET /v1/clients clients:read

Archived clients are left out unless includeArchived is true. updatedSince returns only the clients created or changed at or after that instant — what a sync asks for.

ParameterInType
page query integer
pageSize query integer
updatedSince query string (date-time)
includeArchived query boolean

Answers 200 with a page of Client; may refuse 403.

curl -X GET 'https://smg-api.smgtools.com/v1/clients' \
  -H 'Authorization: Bearer smgk_…'

Add a client

POST /v1/clients clients:write

The client's source is Integration, labelled with the key's name, and its history says the key added it. Send an Idempotency-Key to make a retry safe.

FieldTypeNotes
name (required) string
businessName string
email string
phone string
address string
notes string

Answers 201 with Client; may refuse 403, 409, 422.

curl -X POST 'https://smg-api.smgtools.com/v1/clients' \
  -H 'Authorization: Bearer smgk_…' \
  -H 'Idempotency-Key: 7c1f0d52-first-try' \
  -H 'Content-Type: application/json' \
  -d '{"name":"Marisol Rivera"}'

One client

GET /v1/clients/{id} clients:read

ParameterInType
id (required) path string (uuid)

Answers 200 with Client; may refuse 403, 404.

curl -X GET 'https://smg-api.smgtools.com/v1/clients/3f2c9a1e-5b7d-4c2a-9e61-0a8d4b2c7f10' \
  -H 'Authorization: Bearer smgk_…'

Change a client's details

PATCH /v1/clients/{id} clients:write

A field left out is left as it is. An archived client cannot be changed.

ParameterInType
id (required) path string (uuid)
FieldTypeNotes
name string
businessName string
email string
phone string
address string
notes string

Answers 200 with Client; may refuse 403, 404, 422.

curl -X PATCH 'https://smg-api.smgtools.com/v1/clients/3f2c9a1e-5b7d-4c2a-9e61-0a8d4b2c7f10' \
  -H 'Authorization: Bearer smgk_…' \
  -H 'Content-Type: application/json' \
  -d '{"name":"Marisol Rivera","businessName":"Marisol Rivera"}'

The Client record

FieldTypeNotes
id string (uuid)
name string
businessName string, or null
email string, or null
phone string, or null
address string, or null
notes string, or null
archived boolean Archived clients are hidden from the business's lists and are listed here only when asked for.
source string The door the client came through, such as Integration, PublicPageDirect or Manual.
sourceLabel string, or null
createdAt string (date-time)
updatedAt string (date-time), or null

Jobs

The business's jobs, a page at a time, oldest first

GET /v1/jobs jobs:read

Narrow by status and clientId. Archived jobs are left out unless includeArchived is true. updatedSince returns only the jobs created or changed at or after that instant.

ParameterInType
page query integer
pageSize query integer
updatedSince query string (date-time)
status query string
clientId query string (uuid)
includeArchived query boolean

Answers 200 with a page of Job; may refuse 403.

curl -X GET 'https://smg-api.smgtools.com/v1/jobs' \
  -H 'Authorization: Bearer smgk_…'

Create a job for one of the business's clients

POST /v1/jobs jobs:write

The job's history says the key created it, and it goes on the business's board, calendar and webhooks exactly as one created in the console. Send an Idempotency-Key to make a retry safe.

FieldTypeNotes
clientId (required) string (uuid)
title (required) string
description string
number string Left out, the business's next number is given.
jobTypeId string (uuid) Left out, the business's default job type.
status string Left out, Planning. EnRoute and OnSite are the crew's to report and are refused.
startDate string (date)
targetCompletionDate string (date)
agreedPrice number
location string
priority string
notes string
customerReference string

Answers 201 with Job; may refuse 403, 409, 422.

curl -X POST 'https://smg-api.smgtools.com/v1/jobs' \
  -H 'Authorization: Bearer smgk_…' \
  -H 'Idempotency-Key: 7c1f0d52-first-try' \
  -H 'Content-Type: application/json' \
  -d '{"clientId":"3f2c9a1e-5b7d-4c2a-9e61-0a8d4b2c7f10","title":"Water heater swap"}'

One job

GET /v1/jobs/{id} jobs:read

ParameterInType
id (required) path string (uuid)

Answers 200 with Job; may refuse 403, 404.

curl -X GET 'https://smg-api.smgtools.com/v1/jobs/3f2c9a1e-5b7d-4c2a-9e61-0a8d4b2c7f10' \
  -H 'Authorization: Bearer smgk_…'

Change a job's details

PATCH /v1/jobs/{id} jobs:write

A field left out is left as it is; an empty string clears a text field. The status changes on its own route.

ParameterInType
id (required) path string (uuid)
FieldTypeNotes
clientId string (uuid)
title string
description string
number string
jobTypeId string (uuid)
startDate string (date)
targetCompletionDate string (date)
agreedPrice number
location string
priority string
notes string
customerReference string

Answers 200 with Job; may refuse 403, 404, 422.

curl -X PATCH 'https://smg-api.smgtools.com/v1/jobs/3f2c9a1e-5b7d-4c2a-9e61-0a8d4b2c7f10' \
  -H 'Authorization: Bearer smgk_…' \
  -H 'Content-Type: application/json' \
  -d '{"clientId":"3f2c9a1e-5b7d-4c2a-9e61-0a8d4b2c7f10","title":"Water heater swap"}'

Move a job to another status, as the office does

POST /v1/jobs/{id}/status jobs:write

The same checks as the console's: a job with a required checklist step unanswered cannot be Completed without a checklistOverrideReason (checklist_outstanding), and EnRoute and OnSite are the crew's to report.

ParameterInType
id (required) path string (uuid)
FieldTypeNotes
status (required) string Planning, Approved, InProgress, OnHold, Completed or Closed.
actualCompletionDate string (date) When it was finished, for Completed; today in the business's time zone when left out.
checklistOverrideReason string Why the job may be finished with its checklist incomplete. Refused without one while anything required is outstanding.

Answers 200 with Job; may refuse 403, 404, 422.

curl -X POST 'https://smg-api.smgtools.com/v1/jobs/3f2c9a1e-5b7d-4c2a-9e61-0a8d4b2c7f10/status' \
  -H 'Authorization: Bearer smgk_…' \
  -H 'Content-Type: application/json' \
  -d '{"status":"InProgress"}'

The Job record

FieldTypeNotes
id string (uuid)
number string The job's number in the business, such as J-0042.
clientId string (uuid)
jobTypeId string (uuid)
title string
description string, or null
status string One of Planning, Approved, InProgress, OnHold, EnRoute, OnSite, Completed, Closed. EnRoute and OnSite are the crew's to report.
startDate string (date), or null
targetCompletionDate string (date), or null
actualCompletionDate string (date), or null
billingBasis string, or null AgreedPrice, AsItGoes, or null when the business has not said.
agreedPrice number, or null The price agreed for the job, when it is billed at one.
location string, or null
priority string, or null Low, Medium, High or Urgent.
notes string, or null
customerReference string, or null The customer's own reference for the work — a purchase order, a work order.
archived boolean
createdAt string (date-time)
updatedAt string (date-time), or null

Leads

The business's leads, a page at a time, oldest first

GET /v1/leads leads:read

Narrow by status. Archived leads are left out unless includeArchived is true or the status asked for is Archived. updatedSince returns only the leads created, contacted, converted or archived at or after that instant.

ParameterInType
page query integer
pageSize query integer
updatedSince query string (date-time)
status query string
includeArchived query boolean

Answers 200 with a page of Lead; may refuse 403.

curl -X GET 'https://smg-api.smgtools.com/v1/leads' \
  -H 'Authorization: Bearer smgk_…'

Add a lead

POST /v1/leads leads:write

Through the same door as the business's public form and its inbound lead address: the same rules, every problem named at once. Its source is Integration, labelled with the key's name. Send an Idempotency-Key to make a retry safe.

FieldTypeNotes
name string
email string
phone string
address string
descriptionOfWork string
preferredContactMethod string Email, Phone or Text.
preferredTimeOfDay string Morning, Afternoon or Evening.
preferredDate string (date)
preferredTimeWindow string

Answers 201 with Lead; may refuse 403, 409, 422.

curl -X POST 'https://smg-api.smgtools.com/v1/leads' \
  -H 'Authorization: Bearer smgk_…' \
  -H 'Idempotency-Key: 7c1f0d52-first-try' \
  -H 'Content-Type: application/json' \
  -d '{"name":"Marisol Rivera","email":"marisol@example.com"}'

One lead

GET /v1/leads/{id} leads:read

ParameterInType
id (required) path string (uuid)

Answers 200 with Lead; may refuse 403, 404.

curl -X GET 'https://smg-api.smgtools.com/v1/leads/3f2c9a1e-5b7d-4c2a-9e61-0a8d4b2c7f10' \
  -H 'Authorization: Bearer smgk_…'

Move a lead, as the Leads page does

POST /v1/leads/{id}/status leads:write

New to Contacted and back, to Archived from any, and an archived lead restored to Contacted. A lead becomes Converted only by becoming a client.

ParameterInType
id (required) path string (uuid)
FieldTypeNotes
status (required) string New, Contacted or Archived.

Answers 200 with Lead; may refuse 403, 404, 422.

curl -X POST 'https://smg-api.smgtools.com/v1/leads/3f2c9a1e-5b7d-4c2a-9e61-0a8d4b2c7f10/status' \
  -H 'Authorization: Bearer smgk_…' \
  -H 'Content-Type: application/json' \
  -d '{"status":"InProgress"}'

The Lead record

FieldTypeNotes
id string (uuid)
name string
email string, or null
phone string, or null
address string, or null
descriptionOfWork string, or null
preferredContactMethod string, or null
preferredTimeOfDay string, or null
preferredDate string (date), or null
preferredTimeWindow string, or null
source string The door the lead came through, such as Integration, PublicPageDirect, Referral or InboundCall.
sourceLabel string, or null What that door was called — for a lead a key made, the key's name.
status string New, Contacted, Converted or Archived.
clientId string (uuid), or null The client the lead became, or belongs to.
createdAt string (date-time)
contactedAt string (date-time), or null
convertedAt string (date-time), or null
archivedAt string (date-time), or null

Appointments

The business's appointments, a page at a time, oldest booked first

GET /v1/appointments appointments:read

Narrow to dates from and to, inclusive, on the business's own calendar. Cancelled appointments are left out unless includeCancelled is true. updatedSince returns only the appointments booked, changed or cancelled at or after that instant.

ParameterInType
page query integer
pageSize query integer
updatedSince query string (date-time)
from query string (date)
to query string (date)
includeCancelled query boolean

Answers 200 with a page of Appointment; may refuse 403.

curl -X GET 'https://smg-api.smgtools.com/v1/appointments' \
  -H 'Authorization: Bearer smgk_…'

Book an appointment against one of the business's jobs or clients

POST /v1/appointments appointments:write

On the business's own clock and its calendar, with its reminders and, when asked, its email to the client. Send an Idempotency-Key to make a retry safe.

FieldTypeNotes
title (required) string
date (required) string (date)
startTime string (time) Needed unless allDay is true.
endTime string (time) An hour after the start when left out.
allDay (required) boolean
location string
notes string
jobId string (uuid)
clientId string (uuid)
reminderEnabled boolean Left out, true.
notifyClient boolean Send the client an email that the visit is booked. Left out, false.

Answers 201 with Appointment; may refuse 403, 409, 422.

curl -X POST 'https://smg-api.smgtools.com/v1/appointments' \
  -H 'Authorization: Bearer smgk_…' \
  -H 'Idempotency-Key: 7c1f0d52-first-try' \
  -H 'Content-Type: application/json' \
  -d '{"title":"Water heater swap","date":"2026-10-01","startTime":"09:00:00","allDay":false}'

One appointment

GET /v1/appointments/{id} appointments:read

ParameterInType
id (required) path string (uuid)

Answers 200 with Appointment; may refuse 403, 404.

curl -X GET 'https://smg-api.smgtools.com/v1/appointments/3f2c9a1e-5b7d-4c2a-9e61-0a8d4b2c7f10' \
  -H 'Authorization: Bearer smgk_…'

Reschedule an appointment, or change its details

PATCH /v1/appointments/{id} appointments:write

A field left out is left as it is; an empty string clears the location or the notes; allDay true drops the times.

ParameterInType
id (required) path string (uuid)
FieldTypeNotes
title string
date string (date)
startTime string (time)
endTime string (time)
allDay boolean
location string
notes string
reminderEnabled boolean

Answers 200 with Appointment; may refuse 403, 404, 422.

curl -X PATCH 'https://smg-api.smgtools.com/v1/appointments/3f2c9a1e-5b7d-4c2a-9e61-0a8d4b2c7f10' \
  -H 'Authorization: Bearer smgk_…' \
  -H 'Content-Type: application/json' \
  -d '{"title":"Water heater swap","date":"2026-10-01"}'

Cancel an appointment

POST /v1/appointments/{id}/cancel appointments:write

It stays on record as cancelled; nothing is deleted. Cancelling a cancelled appointment answers it as it is.

ParameterInType
id (required) path string (uuid)

Answers 200 with Appointment; may refuse 403, 404.

curl -X POST 'https://smg-api.smgtools.com/v1/appointments/3f2c9a1e-5b7d-4c2a-9e61-0a8d4b2c7f10/cancel' \
  -H 'Authorization: Bearer smgk_…'

The Appointment record

FieldTypeNotes
id string (uuid)
title string
date string (date)
startTime string (time), or null
endTime string (time), or null
allDay boolean
start string (date-time), or null
end string (date-time), or null
location string, or null
notes string, or null
jobId string (uuid), or null
clientId string (uuid), or null
reminderEnabled boolean Whether the client is sent a reminder before the visit.
cancelled boolean
createdAt string (date-time)
updatedAt string (date-time), or null
cancelledAt string (date-time), or null

Invoices

The business's invoices, with their lines, payments and balance, a page at a time

GET /v1/invoices invoices:read

Narrow by status, clientId and jobId. updatedSince returns only the invoices raised or changed at or after that instant. Nothing on v1 writes an invoice.

ParameterInType
page query integer
pageSize query integer
updatedSince query string (date-time)
status query string
clientId query string (uuid)
jobId query string (uuid)

Answers 200 with a page of Invoice; may refuse 403.

curl -X GET 'https://smg-api.smgtools.com/v1/invoices' \
  -H 'Authorization: Bearer smgk_…'

One invoice, with its lines, payments and balance

GET /v1/invoices/{id} invoices:read

ParameterInType
id (required) path string (uuid)

Answers 200 with Invoice; may refuse 403, 404.

curl -X GET 'https://smg-api.smgtools.com/v1/invoices/3f2c9a1e-5b7d-4c2a-9e61-0a8d4b2c7f10' \
  -H 'Authorization: Bearer smgk_…'

The Invoice record

FieldTypeNotes
id string (uuid)
number string The invoice's number in the business, such as INV-0042.
clientId string (uuid)
jobId string (uuid), or null
status string Draft, Sent, Viewed, PartiallyPaid, Paid, Overdue or Voided.
issueDate string (date) The date printed on the invoice.
dueDate string (date), or null
lines array of InvoiceLine
subtotal number
taxRate number, or null The tax rate as a percentage, such as 8.25; null when the invoice is not taxed.
tax number
total number
amountPaid number Everything paid, tips excluded.
balance number What is still owed: the total less what has been paid.
payments array of Payment
customerReference string, or null
notes string, or null
sentAt string (date-time), or null
paidAt string (date-time), or null
voidedAt string (date-time), or null
createdAt string (date-time)
updatedAt string (date-time), or null

The InvoiceLine record

FieldTypeNotes
description string
quantity number, or null
unitPrice number, or null
hours number, or null
hourlyRate number, or null
total number

The Payment record

FieldTypeNotes
id string (uuid)
amount number
tip number A tip given with the payment, on top of the amount.
method string How it was paid, such as Card, Cash, Check or BankTransfer.
receivedAt string (date-time)
voided boolean A voided payment no longer counts toward what is paid.

Webhooks

The events a subscription may be sent, with the fields each carries

GET /v1/webhooks/events any key

Answers 200 with WebhookEvent.

curl -X GET 'https://smg-api.smgtools.com/v1/webhooks/events' \
  -H 'Authorization: Bearer smgk_…'

The subscriptions this key made

GET /v1/webhooks webhooks:write

Only this key's: a subscription a person or another key made is theirs.

Answers 200 with Webhook; may refuse 403.

curl -X GET 'https://smg-api.smgtools.com/v1/webhooks' \
  -H 'Authorization: Bearer smgk_…'

Subscribe an address to events

POST /v1/webhooks webhooks:write

The console's rules: an https address on the public internet, events from the catalog, at most ten subscriptions per business; the address is checked again before every delivery. The signing secret is in this answer and never again. Send an Idempotency-Key to make a retry safe.

FieldTypeNotes
url (required) string
events (required) array of string

Answers 201 with CreatedWebhook; may refuse 403, 409, 422.

curl -X POST 'https://smg-api.smgtools.com/v1/webhooks' \
  -H 'Authorization: Bearer smgk_…' \
  -H 'Idempotency-Key: 7c1f0d52-first-try' \
  -H 'Content-Type: application/json' \
  -d '{"url":"https://example.com/hooks/smg","events":["lead.created"]}'

Remove a subscription this key made

DELETE /v1/webhooks/{id} webhooks:write

Nothing already sent is recalled. A subscription the key did not make is not found.

ParameterInType
id (required) path string (uuid)

Answers 204; may refuse 403, 404.

curl -X DELETE 'https://smg-api.smgtools.com/v1/webhooks/3f2c9a1e-5b7d-4c2a-9e61-0a8d4b2c7f10' \
  -H 'Authorization: Bearer smgk_…'

The WebhookEvent record

FieldTypeNotes
name string
description string
fields array of string

The Webhook record

FieldTypeNotes
id string (uuid)
url string
events array of string The events it is sent, by name from the catalog at /v1/webhooks/events.
status string Active; Paused, when the platform stopped after failed deliveries and the business has to resume it; or Disabled.
createdAt string (date-time)

The CreatedWebhook record

FieldTypeNotes
webhook Webhook
secret string Verifies each delivery's X-SmgTools-Signature: t=<timestamp>,v1=<hex>, where v1 is HMAC-SHA256 of <timestamp>.<body> under this secret.

The business’s side — making a key, choosing what it may do, seeing what it has been doing and revoking it — is in the help page Connect a program with an API key. Use of the API is on the Terms of Use, section 8.13.