Help Centre

The Mucka API

Technical reference for Mucka's API, which powers the Mucka app on Zapier. Covers OAuth 2 sign-in, every endpoint, the webhook events and their payloads, permissions, errors and rate limits.

Walkthrough coming soon
90-second video. We're recording these now.

This is the technical reference for Mucka's API. It is written for developers and for Zapier's review team. If you just want to connect Mucka to your other apps, read Connect Zapier instead: you don't need anything on this page to do that.

Who the API is for

The API exists to power the Mucka app on Zapier. Everything the Zapier app does goes through the endpoints on this page, and nothing else.

There are no self-serve API keys. An app can only connect through OAuth 2, and only if Mucka has registered it as an OAuth client with a client ID, a client secret and its exact redirect addresses. Today the only registered client is Zapier. If you'd like to build on the API for something else, get in touch.

A few rules hold for every call, whatever the app sends:

  • Nothing reaches a customer. No endpoint sends an email, text or quote. Jobs are created unscheduled or pencilled in, never confirmed, and quotes are always drafts that the business checks and sends itself.
  • A connection acts as the person who approved it, with that person's permissions in that one workspace. There are no separate scopes.
  • Every call is checked again, not just at connect time. If the person loses access to the workspace, or the workspace moves to the Free plan, the connection stops working straight away.

Basics

  • Base URL: https://app.mucka.ai
  • Format: JSON in and out. Send Content-Type: application/json on requests with a body.
  • Authentication: an OAuth 2 access token in the Authorization header, as Authorization: Bearer ACCESS_TOKEN.
  • IDs are UUIDs. Timestamps are ISO 8601 in UTC, for example 2026-09-24T09:00:00Z. Money is in pounds, as numbers (240 means £240.00).
  • Field names are snake_case. A field with no value comes back as null rather than being left out.

Connecting with OAuth 2

Mucka is an OAuth 2 provider using the authorisation code grant, with refresh tokens. Implicit, client-credentials and password grants are not supported, and there are no scopes.

Step 1: send the person to the consent screen

Open this address in the person's browser:

GET https://app.mucka.ai/oauth/authorize
  ?response_type=code
  &client_id=YOUR_CLIENT_ID
  &redirect_uri=YOUR_REGISTERED_REDIRECT_URI
  &state=YOUR_STATE
  • client_id (required): the client ID Mucka issued to your app.
  • redirect_uri (required): must match one of your registered redirect addresses exactly, character for character. No prefixes or wildcards.
  • response_type: code. Anything else is refused.
  • state (recommended): any value. It is sent back to you unchanged so you can check the response belongs to your request.

If the person isn't signed in to Mucka, they're asked to sign in first and then brought back to the consent screen with your parameters intact.

The consent screen lists every workspace the person belongs to. They can only connect one where they hold the manage workspace permission (by default, the Owner role) and which is on a paid plan. Other workspaces are shown with the reason they can't be picked.

If the client_id is unknown or the redirect_uri isn't registered, Mucka shows an error on its own page and does not redirect, so it can never be used as an open redirect.

Step 2: receive the code

When the person presses Allow, Mucka redirects (HTTP 303) to your redirect address with a code:

YOUR_REDIRECT_URI?code=AUTHORISATION_CODE&state=YOUR_STATE

If they press Cancel:

YOUR_REDIRECT_URI?error=access_denied&state=YOUR_STATE

The code lasts 10 minutes, works once, and only for the same client and redirect address it was issued to.

Step 3: swap the code for tokens

POST https://app.mucka.ai/api/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=AUTHORISATION_CODE
&redirect_uri=YOUR_REGISTERED_REDIRECT_URI
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET

The token endpoint accepts a form-encoded body (the OAuth default) or JSON. Client credentials can go in the body, as above, or in an HTTP Basic Authorization header.

A successful response:

{
  "access_token": "…",
  "refresh_token": "…",
  "token_type": "Bearer",
  "expires_in": 3600
}

Tokens are opaque strings. Mucka stores only a hash of each one, so a lost token can't be looked up, only replaced. Token responses are sent with Cache-Control: no-store.

Step 4: refresh when the access token expires

Access tokens last one hour (expires_in: 3600). A call made with an expired token gets a 401, and you should refresh:

POST https://app.mucka.ai/api/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token
&refresh_token=REFRESH_TOKEN
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET

The response has the same shape as in step 3. Every refresh replaces both tokens: the old refresh token stops working as soon as the new pair is issued, so always keep the newest one. Refresh tokens don't expire on a timer; they stop working when the connection is disconnected.

Token endpoint errors

Errors from the token endpoint follow the OAuth 2 format:

{ "error": "invalid_grant", "error_description": "The code is invalid, expired or already used." }
  • 401 invalid_client: unknown client ID or wrong client secret.
  • 400 invalid_request: a required field is missing (code and redirect_uri, or refresh_token).
  • 400 invalid_grant: the code is invalid, expired, already used, or was issued to a different client or redirect address; or the refresh token is invalid or its connection was disconnected.
  • 400 unsupported_grant_type: only authorization_code and refresh_token are accepted.
  • 500 server_error: try again shortly.

Disconnecting

The business can disconnect at any time in Mucka under Settings → Integrations → Zapier. From that moment the connection's tokens are refused and its webhook subscriptions are switched off. There is no revoke endpoint for the app to call.

Errors, permissions and limits

Error format

Every error from the /api/v1 endpoints has the same shape:

{
  "error": "permission_denied",
  "message": "The Mucka user this connection belongs to can't add clients."
}

error is a stable code for your program to act on. message is a plain-English sentence written for the business owner, and is safe to show them as it is. The Zapier app shows message word for word.

  • 400 invalid_request: the request is missing something or has a value Mucka can't read. The message says which.
  • 401 invalid_token: no token, an unknown token, an expired token, or a disconnected connection. Refresh and try again; if the refresh fails, the person needs to reconnect. The response carries a WWW-Authenticate: Bearer header.
  • 402 paid_plan_required: the workspace is no longer on a paid plan.
  • 403 access_revoked: the person who approved the connection is no longer a member of the workspace, or has been suspended.
  • 403 permission_denied: that person's role doesn't allow this action.
  • 404 not_found: no such record in this workspace, or one this connection isn't allowed to see. The two look the same on purpose.
  • 409 name_clash: see Create a client.
  • 409 conflict: the record couldn't be created as it stands. The message says why.
  • 429 rate_limited: too many requests. Wait for the number of seconds in the Retry-After header.
  • 500 server_error: something went wrong on Mucka's side. Try again in a minute.

Permissions

A connection can do exactly what its person can do in the Mucka app, using the same permissions as the business's roles:

  • GET /api/v1/me: any live connection.
  • GET /api/v1/clients: view clients.
  • POST /api/v1/clients: create clients.
  • GET /api/v1/jobs: view all jobs, or view own jobs (which only finds jobs assigned to that person).
  • POST /api/v1/jobs: create jobs.
  • POST /api/v1/jobs/{id}/notes: any person who can open that job in Mucka (view all jobs, or view own jobs when they are assigned to it or helping on it).
  • POST /api/v1/quotes: create quotes.
  • Webhooks: client.created needs view clients; job.created and job.completed need view all jobs or view own jobs; quote.accepted needs view quotes; invoice.paid needs view invoices.

Rate limit

Each connection can make 120 requests a minute. Past that, Mucka answers 429 rate_limited with a Retry-After header.

Who is connected

GET /api/v1/me

Returns the workspace and the person the connection belongs to. Zapier calls it to test a connection and to label it, so somebody with two workspaces can tell their connections apart.

{
  "workspace": { "id": "b3c1…", "name": "Ryan's Plumbing" },
  "user": {
    "id": "7f2a…",
    "name": "Ryan Smith",
    "email": "ryan@example.com",
    "role": "Owner"
  }
}

Clients

The client object

Every client endpoint and the client.created event return clients in this shape:

  • id: the client's ID.
  • name: the name to show. The company name for a commercial client, otherwise first and last name.
  • first_name, last_name: the person's name. Always null for a commercial client.
  • company_name: the business name, for a commercial client.
  • contact_name: the person to deal with at a commercial client.
  • type: residential or commercial.
  • email, phone: the main email address and phone number.
  • address, postcode: the client's address.
  • created_at: when the client was added.
{
  "id": "c0ffee00-0000-4000-8000-000000000001",
  "name": "Nigel Barnes",
  "first_name": "Nigel",
  "last_name": "Barnes",
  "company_name": null,
  "contact_name": null,
  "type": "residential",
  "email": "nigel@example.com",
  "phone": "07700 900123",
  "address": "1 High Street, Shrewsbury",
  "postcode": "SY1 1AA",
  "created_at": "2026-09-24T09:00:00Z"
}

GET /api/v1/clients (find clients)

Searches the workspace's clients. At least one filter is required: this is a search, not a way to list every client.

  • email: an exact match on any of the client's email addresses. Case doesn't matter.
  • phone: an exact match on the client's main or second phone number. Any UK format works, such as 07700 900123 or +44 7700 900123.
  • name: matches the words of the name, in any order.

If you send more than one filter, a client has to match all of them. Up to 10 clients are returned. Clients that have been merged into another record are never returned.

GET /api/v1/clients?email=nigel@example.com
{ "clients": [ { "id": "c0ffee00-…", "name": "Nigel Barnes", "…": "…" } ] }

An empty clients array means nothing matched. It is not an error.

POST /api/v1/clients (create a client)

Adds a client, applying the same checks as adding one by hand in Mucka.

  • first_name, last_name: the person's name.
  • company_name: fill this in for a business. The client is then commercial, and any first and last name you send is kept as the contact.
  • email: must be a valid email address.
  • phone: must be a UK number that can receive a call or text.
  • address, postcode: the client's address.
  • notes: internal notes about the client. Not returned in the client object.
  • create_even_if_name_matches: true to add the client even though somebody of the same name already exists (see below). Defaults to false.

A name is required: either a company name, or at least one of first and last name.

{
  "first_name": "Nigel",
  "last_name": "Barnes",
  "email": "nigel@example.com",
  "phone": "07700 900123",
  "address": "1 High Street, Shrewsbury",
  "postcode": "SY1 1AA"
}

When the client is new, the answer is 201 Created:

{ "client": { "id": "c0ffee00-…", "name": "Nigel Barnes", "…": "…" }, "created": true, "renamed": false }

When the client is already there, meaning the same name and the same phone number or email address, nothing is created. The answer is 200 OK with the existing client, so the same web form arriving twice doesn't make a duplicate:

{
  "client": { "id": "c0ffee00-…", "name": "Nigel Barnes", "…": "…" },
  "created": false,
  "message": "You already have Nigel Barnes on the same email address."
}

When only the name matches, Mucka can't tell whether this is the same person or somebody else, so it refuses with 409 name_clash and lists who it already has:

{
  "error": "name_clash",
  "message": "You already have a Nigel Barnes. Nothing matches on phone or email, so Mucka can't tell whether this is the same person. To add them as somebody new, set \"Create even if the name matches\" to Yes.",
  "existing": [
    { "id": "c0ffee00-…", "name": "Nigel Barnes", "detail": "…" }
  ]
}

To add them anyway, send the same request with "create_even_if_name_matches": true. The new client's surname is then tagged with their postcode, for example Barnes (SY1 1AA), because two clients with identical names can't both reach the business's accounting software. When that happens the response has "renamed": true.

A bad email address or phone number is a 400 invalid_request whose message says which field is wrong.

Jobs

The job object

Every job endpoint and the job.created and job.completed events return jobs in this shape:

  • id: the job's ID.
  • type: the kind of job, such as Boiler Service.
  • status: the business's own name for the job's status. Businesses can rename statuses, so don't compare against this.
  • status_key: the stable meaning of the status: pending, scheduled, active, completed or cancelled. Can be null for a status Mucka can't classify. Use this to tell whether a job is finished.
  • scheduled_start: when the job is booked for, or null if it's unscheduled.
  • pencilled: true if the time is pencilled in rather than confirmed.
  • client_id, property_id: the client and the property the job is for.
  • description: what the work is.
  • notes: private notes on the job.
  • reference: the job's reference number, or the reference it had in the system it was imported from.
  • po_number: the customer's purchase order number.
  • created_at: when the job was created.
{
  "id": "c0ffee00-0000-4000-8000-000000000002",
  "type": "Boiler Service",
  "status": "scheduled",
  "status_key": "scheduled",
  "scheduled_start": "2026-10-01T09:00:00Z",
  "pencilled": false,
  "client_id": "c0ffee00-0000-4000-8000-000000000001",
  "property_id": null,
  "description": "Annual service",
  "notes": null,
  "reference": "J-0042",
  "po_number": null,
  "created_at": "2026-09-24T09:00:00Z"
}

GET /api/v1/jobs (find jobs)

At least one filter is required.

  • reference: an exact match on the job's reference number, the customer's PO number, or the reference it had in the system it was imported from.
  • client_id: that client's jobs.

Up to 10 jobs are returned, newest first. A connection whose person only has view own jobs only finds jobs assigned to them.

GET /api/v1/jobs?reference=J-0042
{ "jobs": [ { "id": "c0ffee00-…", "type": "Boiler Service", "…": "…" } ] }

POST /api/v1/jobs (create a job)

  • type (required): the kind of job, for example Boiler Service or Enquiry.
  • client_id: the client it's for. Must be a client in this workspace.
  • property_id: the property it's at. Must be a property in this workspace.
  • description: what the work is.
  • notes: private notes.
  • preferred_start: a date and time, such as 2026-10-01T09:00:00Z. If sent, the job is pencilled in at that time for the business to confirm. If left out, the job is unscheduled.

These are the only fields accepted. The job is always a single visit with nobody assigned, and it's never a confirmed booking, so the customer isn't told anything until somebody at the business confirms it in Mucka.

{
  "type": "Boiler Service",
  "client_id": "c0ffee00-0000-4000-8000-000000000001",
  "description": "Annual service, customer prefers mornings",
  "preferred_start": "2026-10-01T09:00:00Z"
}

The answer is 201 Created:

{ "job": { "id": "c0ffee00-…", "type": "Boiler Service", "pencilled": true, "…": "…" } }

A preferred_start Mucka can't read, or a client_id or property_id that isn't in this workspace, is a 400 invalid_request.

POST /api/v1/jobs/{id}/notes (add a note to a job)

Adds a note to the job, written as the person who approved the connection. It appears on the job exactly like a note typed in Mucka. Notes are internal and are never shown to the customer.

  • content (required): the note's text, up to 10,000 characters.
{ "content": "Customer says the side gate is locked" }

The answer is 201 Created:

{
  "note": {
    "id": "c0ffee00-0000-4000-8000-000000000005",
    "job_id": "c0ffee00-0000-4000-8000-000000000002",
    "content": "Customer says the side gate is locked",
    "created_at": "2026-09-24T09:00:00Z"
  }
}

A job that doesn't exist, or that the connection's person can't open, is a 404 not_found.

Quotes

POST /api/v1/quotes (create a draft quote)

Creates a quote as a draft. It is never sent: somebody at the business opens it, checks it and sends it themselves. There's no way to create a sent quote through the API.

  • client_id: the client it's for.
  • job_id: the job it's for.
  • property_id: the property it's for. If left out and a job is given, the job's property is used.
  • description: what the quote is for.
  • intro_note: a note to the customer, shown at the top of the quote.
  • lines (required, at least one): the quote's lines. Each line has:
    • description (required): what the line is, up to 1,000 characters.
    • quantity: above 0. Defaults to 1.
    • unit_price: the price for one, in pounds, before VAT. Can't be negative. Defaults to 0.

Any IDs must belong to this workspace. Numbers can be sent as numbers or as text, and a pound sign or commas are ignored, so "£1,200" is read as 1200. VAT is added at the business's default rate.

{
  "client_id": "c0ffee00-0000-4000-8000-000000000001",
  "description": "New combi boiler",
  "intro_note": "Thanks for having us round, here's the price we talked about.",
  "lines": [
    { "description": "Supply and fit combi boiler", "quantity": 1, "unit_price": 1850 },
    { "description": "Magnetic filter", "quantity": 1, "unit_price": "240.00" }
  ]
}

The answer is 201 Created with the draft quote:

{
  "quote": {
    "id": "c0ffee00-0000-4000-8000-000000000003",
    "reference": "Q-0042",
    "status": "draft",
    "description": "New combi boiler",
    "intro_note": "Thanks for having us round, here's the price we talked about.",
    "client_id": "c0ffee00-0000-4000-8000-000000000001",
    "job_id": null,
    "property_id": null,
    "subtotal": 2090,
    "tax": 418,
    "total": 2508,
    "valid_until": null,
    "created_at": "2026-09-24T09:00:00Z"
  }
}

A line Mucka can't read is a 400 invalid_request whose message names the line, for example Line 2 needs a description.

Webhooks

Mucka tells a connected app when something happens, using REST hooks: the app subscribes a URL to an event, Mucka posts to that URL each time the event happens, and the app unsubscribes when it no longer wants them.

The events

  • client.created: a client was added. Clients brought in by an import don't count.
  • job.created: a job was created. Jobs brought in by an import, or mirrored from JobLogic, don't count.
  • job.completed: a job moved into a finished status. It fires when a job enters the finished group, so moving between two finished statuses (for example "to invoice" and "invoice sent") doesn't fire it again. A job that is reopened and finished again fires it again.
  • quote.accepted: a quote's status became accepted, however that happened.
  • invoice.paid: an invoice's status became paid, however it was paid (card, bank, marked paid by hand, or picked up from the business's accounting software).

POST /api/v1/hooks (subscribe)

  • event (required): one of the five events above.
  • target_url (required): where Mucka should post. It must be an https address on the connecting app's own webhook host, which for Zapier is hooks.zapier.com. Any other address is refused.
{ "event": "quote.accepted", "target_url": "https://hooks.zapier.com/hooks/standard/…" }

The answer is 201 Created with the subscription's ID. Keep it: you need it to unsubscribe.

{ "id": "5d0e…" }

The connection's person must be allowed to see what the event is about (see Permissions), or the subscription is refused with 403 permission_denied.

A new subscription only hears about events that happen after it was made. Nothing from before is sent.

DELETE /api/v1/hooks/{id} (unsubscribe)

Removes the subscription. The answer is 204 No Content. A connection can only remove its own subscriptions: any other ID is a 404 not_found.

GET /api/v1/hooks/samples?event=EVENT (example payloads)

Returns up to three recent real records for an event, in exactly the shape a delivery has, so somebody setting up a Zap can map real fields. Zapier uses this as the trigger's "perform list". The same permission rules apply as for a delivery. If the workspace has nothing suitable yet, the list is empty.

[
  {
    "id": "sample-c0ffee00-0000-4000-8000-000000000003",
    "event": "quote.accepted",
    "occurred_at": "2026-09-20T09:00:00Z",
    "data": { "id": "c0ffee00-0000-4000-8000-000000000003", "reference": "Q-0042", "…": "…" }
  }
]

What a delivery looks like

Mucka sends a POST with a JSON body and these headers: Content-Type: application/json and User-Agent: Mucka-Webhooks/1.

{
  "id": "evt_1234",
  "event": "quote.accepted",
  "occurred_at": "2026-09-24T09:00:00Z",
  "data": { "…": "the record, as below" }
}
  • id: the event's ID, not the record's. Use it to spot duplicates. A job finished twice is two events with two IDs.
  • event: which event this is.
  • occurred_at: when it happened.
  • data: the record the event is about, as it is at the moment of delivery.

What's in data:

{
  "id": "c0ffee00-0000-4000-8000-000000000004",
  "reference": "INV-0042",
  "status": "paid",
  "client_id": "c0ffee00-0000-4000-8000-000000000001",
  "job_id": "c0ffee00-0000-4000-8000-000000000002",
  "quote_id": null,
  "subtotal": 200,
  "tax": 40,
  "total": 240,
  "due_date": "2026-10-08",
  "paid_at": "2026-09-24T09:00:00Z",
  "created_at": "2026-09-10T09:00:00Z"
}

Delivery and retries

  • Mucka checks for events to send every minute, so a delivery usually arrives within a minute or so of the event.
  • Any 2xx answer counts as delivered. Answer within 10 seconds, or the attempt counts as failed.
  • A failed delivery is retried after 2, 4, 8, 16 and 32 minutes, then every hour, up to 8 attempts in all (about three hours). After that it's dropped.
  • A 410 Gone answer means the Zap has been removed. Mucka switches that subscription off and stops sending to it.
  • Before every attempt Mucka checks the connection again, just as it does for an API call. If the connection has been disconnected, the person no longer has access, the workspace is off a paid plan, or the person may no longer see that record (for example, a job that has been reassigned away from them), nothing is sent.
  • If one workspace produces more than 300 of the same event within an hour, the extras are held back rather than sent. That many at once means a bulk change, not a normal day's work, and it shouldn't flood anybody's Zaps.
  • Deliveries aren't guaranteed to arrive in the order the events happened.

Last updated

Still stuck? Talk to a human — we answer every one.