{"openapi":"3.1.0","info":{"title":"Earl connector API","version":"1.2.0","description":"Work with a business's Earl: ask him anything in plain words (he books, quotes, logs, files, and answers), or read the calendar, jobs, customers, and invoices as records. Every key acts as the business owner who made it. Writes go through /v1/ask so Earl's checks apply and the owner sees every change. How to speak for Earl: relay what he reports in his words; never invent a number, date, or name that didn't come back from a call; confirm with the owner before anything that touches money or a customer; when a reply is waiting_for_owner, say Earl has it drafted and is waiting for their OK in the app; write in the owner's language (English or Spanish). Tokens come from Connect Earl (OAuth 2.0 authorization code with PKCE at /oauth/authorize and /oauth/token) or from a key the owner makes under Office → Connections.","contact":{"name":"Earl Software","email":"earlsoftwarellc@gmail.com","url":"https://earl.solutions/developers"},"termsOfService":"https://earl.solutions/terms"},"servers":[{"url":"https://api.earl.solutions"}],"security":[{"apiKey":[]}],"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","description":"An API key from Office → Connections in Earl, or a token from Connect Earl, sent as Authorization: Bearer earl_sk_…"},"oauth2":{"type":"oauth2","description":"Connect Earl: authorization code with PKCE. One token is one business.","flows":{"authorizationCode":{"authorizationUrl":"https://api.earl.solutions/oauth/authorize","tokenUrl":"https://api.earl.solutions/oauth/token","scopes":{"business":"Act as the owner of one business"}}}}}},"paths":{"/v1/me":{"get":{"summary":"The business this key belongs to","operationId":"me","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"business":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"trade":{"type":["string","null"]},"timezone":{"type":"string"}}},"plan":{"type":"string"},"bots":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"type":{"type":"string"},"name":{"type":"string"}}}},"today":{"type":"string","description":"YYYY-MM-DD in the business's time zone"}}}}}}}}},"/v1/ask":{"post":{"summary":"Ask Earl anything, in plain words","description":"Book jobs, log hours, move stock, send quotes, answer questions. Waits for the reply (up to 90 seconds) unless wait is false; then poll /v1/runs/{id}. Earl confirms what he did; anything he can't verify he says so instead of guessing.","operationId":"ask","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"What you'd say to Earl"},"conversationId":{"type":"string","description":"From an earlier reply, to continue that thread"},"wait":{"type":"boolean"}},"required":["message"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"runId":{"type":"string"},"conversationId":{"type":"string","description":"Pass it back on the next ask to keep the thread"},"status":{"type":"string","enum":["done","working","waiting_for_owner","failed","queued"],"description":"waiting_for_owner: Earl drafted a change that needs the owner's OK in the app"},"reply":{"type":["string","null"]},"error":{"type":["string","null"]}}}}}}}}},"/v1/runs/{id}":{"get":{"summary":"Where an ask stands","operationId":"run","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"runId":{"type":"string"},"conversationId":{"type":"string","description":"Pass it back on the next ask to keep the thread"},"status":{"type":"string","enum":["done","working","waiting_for_owner","failed","queued"],"description":"waiting_for_owner: Earl drafted a change that needs the owner's OK in the app"},"reply":{"type":["string","null"]},"error":{"type":["string","null"]}}}}}}}}},"/v1/today":{"get":{"summary":"Today: who is where, and what waits for the owner","operationId":"today","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"date":{"type":"string"},"calendar":{"type":"array","items":{"type":"object","properties":{"job":{"type":"string"},"crew":{"type":"string"},"start":{"type":["string","null"]},"end":{"type":["string","null"]},"status":{"type":["string","null"]}}}},"waitingForOwner":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"summary":{"type":"string"},"since":{"type":"string"}}}}}}}}}}}},"/v1/calendar":{"get":{"summary":"Calendar entries in a date range (default: the next two weeks)","operationId":"calendar","parameters":[{"name":"from","in":"query","schema":{"type":"string","description":"YYYY-MM-DD"}},{"name":"to","in":"query","schema":{"type":"string","description":"YYYY-MM-DD"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"},"entries":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"date":{"type":"string","description":"YYYY-MM-DD"},"job":{"type":"string"},"crew":{"type":"array","items":{"type":"string"}},"start":{"type":["string","null"]},"end":{"type":["string","null"]},"status":{"type":["string","null"]},"notes":{"type":["string","null"]}}}}}}}}}}}},"/v1/jobs":{"get":{"summary":"Jobs","operationId":"jobs","parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["open"]}},{"name":"q","in":"query","schema":{"type":"string","description":"Part of a job name"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"jobs":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"customer":{"type":["string","null"]},"address":{"type":["string","null"]},"start":{"type":["string","null"]},"end":{"type":["string","null"]},"status":{"type":["string","null"]},"value":{"type":["number","null"]},"summary":{"type":["string","null"]}}}}}}}}}}}},"/v1/customers":{"get":{"summary":"Customers","operationId":"customers","parameters":[{"name":"q","in":"query","schema":{"type":"string","description":"Part of a name or phone"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"customers":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"phone":{"type":["string","null"]},"email":{"type":["string","null"]},"address":{"type":["string","null"]}}}}}}}}}}}},"/v1/brief":{"get":{"summary":"The whole day in one call","description":"Today's calendar (who is on what, what has nobody), the rest of the week, who on the crew is free, the to-do list with Earl's own proposal for each item, what's waiting for the owner's OK, unpaid invoices and what's overdue, what the crew is owed, open quotes, and a one-line summary of yesterday. Start here when the owner asks how things are.","operationId":"brief","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"date":{"type":"string","description":"YYYY-MM-DD"},"headline":{"type":"string","description":"One sentence: jobs today, what's waiting, what's unpaid, what's owed"},"spoken":{"type":"string","description":"The day as a short paragraph to read out loud: who is on what, what is waiting for a yes, what is unpaid and owed, mail waiting, stock low"},"yesterday":{"type":["string","null"]},"mail":{"type":"object","properties":{"needReply":{"type":"number"},"today":{"type":"number"}}},"stockLow":{"type":"number"},"today":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"date":{"type":"string","description":"YYYY-MM-DD"},"job":{"type":"string"},"crew":{"type":"array","items":{"type":"string"}},"start":{"type":["string","null"]},"end":{"type":["string","null"]},"status":{"type":["string","null"]}}}},"unassignedToday":{"type":"array","items":{"type":"string","description":"Jobs today with nobody on them"}},"restOfWeek":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"date":{"type":"string","description":"YYYY-MM-DD"},"job":{"type":"string"},"crew":{"type":"array","items":{"type":"string"}},"start":{"type":["string","null"]},"end":{"type":["string","null"]},"status":{"type":["string","null"]}}}},"crewFree":{"type":"array","items":{"type":"string"}},"todo":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"kind":{"type":"string"},"text":{"type":"string"},"detail":{"type":["string","null"]},"earlProposes":{"type":["string","null"]}}}},"waitingForOwner":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Approval id, for /v1/approvals/{id}/decide"},"summary":{"type":"string"},"since":{"type":"string"}}}},"money":{"type":"object","properties":{"unpaidInvoices":{"type":"array","items":{"type":"object","properties":{"number":{"type":"string"},"customer":{"type":"string"},"balance":{"type":"number"},"due":{"type":["string","null"]}}}},"unpaidTotal":{"type":"number"},"overdueCount":{"type":"number"},"owedToCrew":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"amount":{"type":"number"}}}},"owedToCrewTotal":{"type":"number"},"quotesOpen":{"type":"number"}}}}}}}}}}},"/v1/approvals":{"get":{"summary":"What Earl is waiting on the owner for","description":"Earl pauses anything over the owner's limits (money, new customers, sending things) and asks. These are those items. Decide them with /v1/approvals/{id}/decide.","operationId":"approvals","parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["pending","decided"]}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"approvals":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"bot":{"type":"string"},"summary":{"type":"string","description":"What Earl wants to do, in plain words"},"question":{"type":"string","description":"The same as a yes/no question for the owner; a yes is decide with approve"},"status":{"type":"string","enum":["pending","approved","rejected"]},"since":{"type":"string"},"decidedAt":{"type":["string","null"]}}}}}}}}}}}},"/v1/approvals/{id}/decide":{"post":{"summary":"Approve or reject a waiting item","description":"Approve: Earl carries it out now. Reject: he drops it and tells the owner. Only the owner (whose key this is) should trigger this; confirm with them first when the summary involves money or a customer.","operationId":"decideApproval","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"decision":{"type":"string","enum":["approve","reject"]},"note":{"type":"string","description":"Optional word for Earl, e.g. 'but Thursday, not Friday'"}},"required":["decision"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"}}}}}}}}},"/v1/search":{"get":{"summary":"Find anything: customers, jobs, invoices, quotes, calendar, crew","description":"Case-insensitive substring match across the records. Use it to resolve a name before an action ('the Nguyen job', 'Mike's number').","operationId":"search","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string","description":"Two or more characters"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"q":{"type":"string"},"hits":{"type":"array","items":{"type":"object","properties":{"sheet":{"type":"string","enum":["customers","jobs","invoices","quotes","schedule","crew"]},"id":{"type":"string"},"title":{"type":"string"},"fields":{"type":"object","additionalProperties":{"type":"string"}}}}}}}}}}}}},"/v1/actions/book":{"post":{"summary":"Book a job on the calendar","description":"Earl puts the job on the Schedule (and on Jobs if it's new), checks the crew are free, and confirms. Over the owner's limits he drafts it and returns waiting_for_owner.","operationId":"bookJob","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"job":{"type":"string","description":"What the job is, in the business's own words"},"customer":{"type":"string"},"address":{"type":"string"},"date":{"type":"string","description":"YYYY-MM-DD"},"start":{"type":"string","description":"e.g. 8:00"},"end":{"type":"string","description":"e.g. 16:00"},"crew":{"type":"array","items":{"type":"string","description":"Names from the Crew sheet"}},"notes":{"type":"string"},"wait":{"type":"boolean","default":true,"description":"Wait up to 90 s for Earl to finish (default). false: return the run at once and poll /v1/runs/{id}."}},"required":["job","date"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"allOf":[{"type":"object","properties":{"runId":{"type":"string"},"conversationId":{"type":"string","description":"Pass it back on the next ask to keep the thread"},"status":{"type":"string","enum":["done","working","waiting_for_owner","failed","queued"],"description":"waiting_for_owner: Earl drafted a change that needs the owner's OK in the app"},"reply":{"type":["string","null"]},"error":{"type":["string","null"]}}},{"type":"object","properties":{"action":{"type":"string"}}}]}}}}}}},"/v1/actions/text":{"post":{"summary":"Text a crew member or customer","description":"Earl sends the text word for word from the business's texting number and logs it on the Texts sheet. Texting must be on for the business (Office → Phone & text); otherwise the reply says so.","operationId":"sendText","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"to":{"type":"string","description":"A name on the Crew or Customers sheet, or a phone number"},"text":{"type":"string"},"wait":{"type":"boolean","default":true,"description":"Wait up to 90 s for Earl to finish (default). false: return the run at once and poll /v1/runs/{id}."}},"required":["to","text"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"allOf":[{"type":"object","properties":{"runId":{"type":"string"},"conversationId":{"type":"string","description":"Pass it back on the next ask to keep the thread"},"status":{"type":"string","enum":["done","working","waiting_for_owner","failed","queued"],"description":"waiting_for_owner: Earl drafted a change that needs the owner's OK in the app"},"reply":{"type":["string","null"]},"error":{"type":["string","null"]}}},{"type":"object","properties":{"action":{"type":"string"}}}]}}}}}}},"/v1/actions/call":{"post":{"summary":"Have Earl call someone and connect the owner","description":"The owner's phone rings first; when they answer, Earl dials the customer from the business line so the customer sees the business's number. Needs the business line to be on.","operationId":"placeCall","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"to":{"type":"string","description":"A customer name or a phone number"},"name":{"type":"string"},"about":{"type":"string"},"wait":{"type":"boolean","default":true,"description":"Wait up to 90 s for Earl to finish (default). false: return the run at once and poll /v1/runs/{id}."}},"required":["to"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"allOf":[{"type":"object","properties":{"runId":{"type":"string"},"conversationId":{"type":"string","description":"Pass it back on the next ask to keep the thread"},"status":{"type":"string","enum":["done","working","waiting_for_owner","failed","queued"],"description":"waiting_for_owner: Earl drafted a change that needs the owner's OK in the app"},"reply":{"type":["string","null"]},"error":{"type":["string","null"]}}},{"type":"object","properties":{"action":{"type":"string"}}}]}}}}}}},"/v1/actions/invoice":{"post":{"summary":"Draft (or send) an invoice","description":"Earl writes the invoice with the business's logo and, if send is true, emails it with a pay link. Sending to a new customer or over the owner's limit waits for the owner's OK.","operationId":"createInvoice","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"customer":{"type":"string"},"job":{"type":"string"},"lines":{"type":"array","items":{"type":"object","properties":{"description":{"type":"string"},"amount":{"type":"number"}},"required":["description","amount"]}},"due":{"type":"string","description":"YYYY-MM-DD"},"send":{"type":"boolean","default":false},"wait":{"type":"boolean","default":true,"description":"Wait up to 90 s for Earl to finish (default). false: return the run at once and poll /v1/runs/{id}."}},"required":["customer","lines"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"allOf":[{"type":"object","properties":{"runId":{"type":"string"},"conversationId":{"type":"string","description":"Pass it back on the next ask to keep the thread"},"status":{"type":"string","enum":["done","working","waiting_for_owner","failed","queued"],"description":"waiting_for_owner: Earl drafted a change that needs the owner's OK in the app"},"reply":{"type":["string","null"]},"error":{"type":["string","null"]}}},{"type":"object","properties":{"action":{"type":"string"}}}]}}}}}}},"/v1/actions/remember":{"post":{"summary":"Tell Earl something to remember","description":"A fact about the business, a customer, a preference. Earl keeps it in his notebook and uses it from then on.","operationId":"remember","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"text":{"type":"string"},"wait":{"type":"boolean","default":true,"description":"Wait up to 90 s for Earl to finish (default). false: return the run at once and poll /v1/runs/{id}."}},"required":["text"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"allOf":[{"type":"object","properties":{"runId":{"type":"string"},"conversationId":{"type":"string","description":"Pass it back on the next ask to keep the thread"},"status":{"type":"string","enum":["done","working","waiting_for_owner","failed","queued"],"description":"waiting_for_owner: Earl drafted a change that needs the owner's OK in the app"},"reply":{"type":["string","null"]},"error":{"type":["string","null"]}}},{"type":"object","properties":{"action":{"type":"string"}}}]}}}}}}},"/v1/webhooks":{"get":{"summary":"Webhooks this business has","operationId":"webhooks","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"webhooks":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"url":{"type":"string"},"events":{"type":"array","items":{"type":"string"}},"active":{"type":"boolean"},"failures":{"type":"number"},"lastDeliveredAt":{"type":["string","null"]},"lastError":{"type":["string","null"]},"createdAt":{"type":"string"}}}},"events":{"type":"object","additionalProperties":{"type":"string"},"description":"Every event type and what it means"}}}}}}}},"post":{"summary":"Register a webhook","description":"Earl POSTs JSON {id, type, businessId, createdAt, data} to the URL when something happens: approval.waiting, approval.decided, booking.requested, text.received, mail.received, call.answered, call.taken, quote.answered, invoice.paid. Each delivery carries X-Earl-Event and X-Earl-Signature: sha256=HMAC-SHA256(secret, raw body). Retries with backoff for about four hours; twenty straight failures switch the webhook off. The secret is returned once.","operationId":"createWebhook","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","description":"https only"},"events":{"type":"array","items":{"type":"string","enum":["*","approval.waiting","approval.decided","booking.requested","call.taken","invoice.paid"]}}},"required":["url"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"url":{"type":"string"},"events":{"type":"array","items":{"type":"string"}},"secret":{"type":"string","description":"Shown once"},"createdAt":{"type":"string"}}}}}}}}},"/v1/webhooks/{id}":{"delete":{"summary":"Remove a webhook","operationId":"deleteWebhook","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}}}}}}}}},"/v1/webhooks/{id}/test":{"post":{"summary":"Send a test.ping to a webhook","operationId":"testWebhook","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"note":{"type":"string"}}}}}}}}},"/v1/invoices":{"get":{"summary":"Invoices","operationId":"invoices","parameters":[{"name":"unpaid","in":"query","schema":{"type":"string","enum":["1"]}},{"name":"q","in":"query","schema":{"type":"string","description":"Part of a customer name"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"invoices":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"number":{"type":"string"},"customer":{"type":"string"},"job":{"type":["string","null"]},"issued":{"type":["string","null"]},"due":{"type":["string","null"]},"amount":{"type":"number"},"paid":{"type":"number"},"balance":{"type":"number"},"status":{"type":["string","null"]}}}}}}}}}}}}}}