Ascora
BUSINESS · COMMERCE & FINANCE
Enquiries, customers, quotes, jobs, and invoices in the account they connected.
Acts as the person, not as itself
Each user connects their own account. Every call carries both identities — the agent and the person it is acting for — so the agent can never reach past what that individual can already do.
Credentials never touch the agent
Tokens live in the vault and attach server-side at call time. The agent holds a session, not a secret, and revoking access does not mean rotating a key.
Every call on the record
Who asked, which agent acted, which action ran, and the verdict that let it through — one audit trail across every integration, not one per vendor.
What an agent can do
Each action is granted on its own. An agent allowed to read is not thereby allowed to write, and the scope beside each row is what the acting user must have connected for it to run at all.
ascora_delete_webhooks_by_subscriptionidWRITERemove a webhook subscription so Ascora stops calling its URL. DESTRUCTIVE. DELETE /WebHooks/{SubscriptionID}. ⚠ The path is NOT the one the documentation's heading gives. Measured 2026-09-19 with a valid key: DELETE /WebHooks/Delete/{id} answers 404 "No HTTP resource was found that matches the request URI", while DELETE /WebHooks/{id} answers 200 {"success": true}. The PDF carries both spellings -- the heading says /WebHooks/Delete/<SubscriptionID> and its own worked example on the next line says DELETE /WebHooks/1d930e71-... -- and the example is the real one. ⚠ A 200 here is NOT evidence the subscription existed: a subscription id that matches nothing answers the same 200 {"success": true}, and Ascora publishes no endpoint that lists subscriptions, so the removal cannot be read back.
ascora_get_accounting_getinvoicestosendREADRetrieve the customer invoices dated before a given date that still need to be pushed to an accounting package, with their full line detail. GET /Accounting/GetInvoicesToSend. Part of Ascora's accounting-integration set, which exists for accounts NOT using a package with a direct connection (Xero, MYOB, QuickBooks). ⚠ `priorToDate` is effectively REQUIRED and its absence is spelled as a ROUTING failure, not a validation one: without it Ascora answers 404 "No HTTP resource was found that matches the request URI" (measured 2026-09-19), which reads exactly like a missing route. Answers a bare JSON ARRAY.
ascora_get_accounting_getpaymentstosendREADRetrieve the payments entered in Ascora that still need to be pushed to an accounting package. GET /Accounting/GetPaymentsToSend. Answers a bare JSON ARRAY. Each entry carries the payment, the invoice it was applied to, the customer, any surcharge and the receipt number. `priorToDate` behaves as it does on the invoices read -- the documented example always passes it.
ascora_get_customers_contact_by_idREADRetrieve one contact record by its Ascora contact id. GET /Customers/Contact/{id}. A contact that does not exist answers HTTP 200 with {"success": false, "message": "No matching Contact found based on ID specified <id>"} (measured 2026-09-19), not a 404.
ascora_get_customers_contactsREADSearch contacts across all customers, with paging and `totalPages`/`totalRecords`. GET /Customers/Contacts. Each result carries the `customerId` the contact belongs to.
ascora_get_customers_customer_by_idREADRetrieve one customer record by its Ascora customer id. GET /Customers/Customer/{id}. A customer that does not exist is NOT a 404: Ascora answers HTTP 200 with {"success": false, "message": "No matching Customer found for ID <id>"} (measured 2026-09-19). Read `success`, not the status code.
ascora_get_customers_customersREADSearch the customer list, with filters, paging, and `totalPages`/`totalRecords` on the response. GET /Customers/Customers. Called with no filters it returns the whole customer list a page at a time. This is also the endpoint the connection probe uses. ⚠ The documented `PhoneNumber` filter is NOT offered: measured 2026-09-19 it is IGNORED by Ascora on this endpoint -- `PhoneNumber=99999` (and five other spellings) returned every customer -- so a caller who passed it would read an unfiltered list as a match. Use `FilterText`, which does match phone and mobile numbers, instead. (`PhoneNumber` on `ascora_get_customers_contacts` works and is kept.)
ascora_get_customers_getcontactsforcustomer_by_customeridREADList every contact attached to one customer. GET /Customers/GetContactsForCustomer/{customerId}. A customer id that matches nothing answers 200 with an EMPTY results list and `success: true` -- not the `success: false` envelope the by-id reads use (measured 2026-09-19), so an empty answer here does not distinguish "no contacts" from "no such customer".
ascora_get_inventory_categoriesREADSearch the categories that segment supplies and kits, at either the parent or the sub-category level. GET /Inventory/Categories. A category is in Category 1 or Category 2, and may be a sub-category under a parent (`parentCategoryId`).
ascora_get_inventory_kitsREADSearch kits -- packaged combinations of supplies and labour -- returning markup, pricing method and category for each. GET /Inventory/Kits. The `kitId` on each result is what `ascora_post_quotes_addkits` takes as its `Id`.
ascora_get_inventory_suppliesREADSearch basic price-list items -- materials and services -- returning cost, sell price and category for each. GET /Inventory/Supplies. The `supplyId` on each result is what `ascora_post_quotes_addsupplies` takes as its `Id`.
ascora_get_jobs_job_by_jobnumberREADRetrieve one job by its full job number, with its status, pricing, totals, addresses and customers. GET /Jobs/Job/{JobNumber}. A job that does not exist answers HTTP 200 with {"success": false, "message": "No Matching Job Found (<n>)"} (measured 2026-09-19), not a 404. `jobStatus` is an integer: 0 New, 1 In Progress, 2 Completed, 3 Cancelled, 4 Deferred, 5 Booked, 6 Unscheduled, 7 Ready To Invoice, 8 Closed.
ascora_get_jobs_joblabour_by_jobnumberREADList every labour entry booked against one job -- hours, role, rate, amounts, who did the work and whether it has been invoiced. GET /Jobs/JobLabour/{JobNumber}. A job number that matches nothing answers 200 with {"success": false, "message": "No Matching Job Found (<n>)"} (measured 2026-09-19).
ascora_get_jobs_jobsREADSearch jobs by status, job type, assigned user, customer name or creation date, with paging. GET /Jobs/Jobs. JobStatus DEFAULTS TO OPEN when it is not supplied, so a call with no filters does NOT return every job -- pass JobStatus=ALL for that. This operation is MISSING FROM THE ENDPOINT LEDGER and was recovered from page 52 of the sealed PDF; it answers 200 (measured 2026-09-19).
ascora_get_jobs_jobtimesheets_by_jobnumberREADList every timesheet entry against one job -- start and end times, who, cost and pay rates, meal breaks and whether it has been sent to payroll. GET /Jobs/JobTimesheets/{JobNumber}. Timesheets are the raw time records; `ascora_get_jobs_joblabour_by_jobnumber` is the chargeable labour derived from them. A job number that matches nothing answers 200 with the No Matching Job Found envelope (measured 2026-09-19).
ascora_get_quotes_labourrolesREADList every current labour role -- the kinds of labour that can be added to a quote or job -- with its hourly rate, ordered by name. GET /Quotes/LabourRoles. Answers a bare JSON ARRAY (measured 2026-09-19). The `labourRoleId` on each entry is what `ascora_post_quotes_addlabouritems` takes as its `Id`.
ascora_get_quotes_quote_by_quotenumberREADRetrieve one quotation by its full quote number. GET /Quotes/Quote/{QuoteNumber}. A quote that does not exist answers HTTP 200 with {"success": false, "message": "No Matching Quotation Found (<n>)", "code": "QUOTE-NOT-FOUND"} (measured 2026-09-19), not a 404. To address a SECTION or STAGE of a quote here, spell the number with a DASH instead of a dot -- Q1234-01, not Q1234.01 -- because a dot in a URL path segment is read as a file extension.
ascora_get_quotes_quotesREADSearch quotations by status, job type, assigned user, customer name or creation date, with paging. GET /Quotes/Quotes. QuoteStatus DEFAULTS TO OPEN when it is not supplied, so a call with no filters does NOT return every quote -- pass QuoteStatus=ALL for that.
ascora_get_quotes_standardsectionsREADList the standard Sections available to add to a quote or job, with their display order. GET /Quotes/StandardSections. Answers a bare JSON ARRAY rather than the usual {"success": ..., "results": [...]} envelope (measured 2026-09-19).
ascora_get_quotes_standardstagesREADList the standard Stages available to add to a quote or job, with their display order. GET /Quotes/StandardStages. Answers a bare JSON ARRAY rather than the usual results envelope (measured 2026-09-19).
ascora_get_supplierinvoices_supplierinvoice_by_idREADRetrieve one supplier invoice or credit note by id, with its supplier, totals and line items. GET /SupplierInvoices/SupplierInvoice/{ID}. An id that matches nothing answers HTTP 200 with {"success": false, "message": "No matching Supplier Invoice found for ID <id>"} (measured 2026-09-19), not a 404.
ascora_get_supplierinvoices_supplierinvoicesREADSearch supplier invoices and credit notes by supplier, tracking number or invoice date, with their line items. GET /SupplierInvoices/SupplierInvoices. This is ALSO how you get the supplier invoices still to be sent to an accounting package: pass ToBeSentToAccountsOnly=true. The PDF spells that filter two ways -- `ToBeSentToAccounting` in the filter list on page 59 and `ToBeSentToAccountsOnly` in the Accounting section on page 80 -- and only the second is real. Measured 2026-09-19 through the shipped route against a supplier invoice before and after marking it sent: `ToBeSentToAccountsOnly=true` returned it before and EXCLUDED it after, while `ToBeSentToAccounting=true` returned it both times (Ascora ignores an unknown query parameter silently). The page-59 spelling is therefore not offered.
ascora_get_suppliers_supplier_by_idREADRetrieve one supplier record by its Ascora supplier id. GET /Suppliers/Supplier/{id}. ⚠ Unlike every other by-id read here, a supplier id that matches nothing answers HTTP 500 {"message": "An error has occurred."} rather than a 200 "not found" envelope (measured 2026-09-19 on a well-formed uuid that exists in no account). That 500 is Ascora's, not a fault in the request and not a credential problem -- confirm an id with `ascora_get_suppliers_suppliers` before reading it.
ascora_get_suppliers_suppliersREADSearch suppliers by name, supplier number or business number, with paging. GET /Suppliers/Suppliers. BusinessNumber is an EXACT match with all white space ignored on both sides, so "11222333444" finds a supplier stored as "11 22 33 444"; SupplierName and SupplierNumber are partial matches.
ascora_post_accounting_createpaymentWRITERecord a payment in Ascora against a customer invoice, by invoice NUMBER. POST /Accounting/CreatePayment. Ascora names the refusal rather than just failing, and each has a code: INVOICE-NOT-SPECIFIED, INVOICE-NOT-FOUND, PAYMENT-METHOD-NOT-FOUND, and PAYMENT-EXCEEDS-BALANCE when the amount plus surcharge is more than the invoice still owes. All of them arrive as HTTP 200 with success:false (measured 2026-09-19: an unknown invoice number answers 200 {"success": false, "message": "No Matching Invoice Found (I00000)", "code": "INVOICE-NOT-FOUND"}). `paymentMethod` must already exist in Ascora -- unlike contact roles and lead sources, it is NOT created on demand.
ascora_post_accounting_markinvoicesassentWRITEMark customer invoices as sent to your accounting package, so they stop being returned by the invoices-to-send read. POST /Accounting/MarkInvoicesAsSent. Answers a BARE INTEGER -- the number marked -- rather than a JSON object (measured 2026-09-19: an empty id list answers 200 with the body `0`). ⚠ An id that matches no invoice is NOT counted as 0: it answers Ascora 500 "An error has occurred." (measured 2026-09-19), so take the ids from `ascora_get_accounting_getinvoicestosend` rather than guessing. This marks CUSTOMER invoices; `ascora_post_supplierinvoices_markinvoicesassent` is the SUPPLIER-invoice equivalent.
ascora_post_accounting_markpaymentsassentWRITEMark payments as sent to your accounting package, so they stop being returned by the payments-to-send read. POST /Accounting/MarkPaymentsAsSent. Answers a BARE INTEGER -- the number marked -- rather than a JSON object (measured 2026-09-19: an empty list AND a list holding an id that matches no payment both answer 200 with the body `0`, unlike the invoice form, which answers 500 for an unknown id).
ascora_post_attachments_by_entitytype_by_entityidWRITEAttach a file to an enquiry, job, quotation, task, supplier invoice, invoice, supply or customer. POST /Attachments/{EntityType}/{EntityId}. This is the ONE operation Ascora serves as multipart/form-data rather than JSON, and it is the one tool here that carries bytes. The file is supplied BASE64-ENCODED and BOUNDED -- see `fileContentBase64` -- and the connector decodes it and builds the multipart part; unbounded bytes never enter an MCP argument. Answers {"entityId": "<uuid>", "success": true} where entityId is the new ATTACHMENT's id. Measured 2026-09-19: an EntityId that matches nothing answers 200 with {"success": false, "message": "No Entity found with the specified ID"}, so check `success`.
ascora_post_customers_contactWRITECreate a contact against a customer, or update an existing one. POST /Customers/Contact. Supplying `contactId` updates that contact; omitting it creates a new one. ⚠ AN UPDATE REPLACES THE WHOLE RECORD, IT DOES NOT MERGE: every field omitted from the request is CLEARED. Measured 2026-09-19 -- an update sending only contactId, customerId, firstName, lastName and mobileNumber blanked the contact's emailAddress and contactRole. Read the contact first (`ascora_get_customers_contact_by_id`) and send it back whole with your changes. The response's `message` is "Contact Created/Updated" either way, so read the returned `contactId` to tell them apart. `contactRole` is the NAME of the role and Ascora CREATES the role if it cannot find it -- a typo here silently adds a new contact role to the account.
ascora_post_customers_customerWRITECreate a customer, or update an existing one by id. POST /Customers/Customer. Supplying `customerId` updates that customer; if no customer matches the id Ascora answers 200 with {"customer": null, "success": false, "message": "No Customer could be found matching ID <id>"} rather than creating one. Omitting `customerId` always creates. ⚠ AN UPDATE REPLACES THE WHOLE RECORD, IT DOES NOT MERGE: every field omitted from the request is CLEARED. Measured 2026-09-19 -- an update sending only customerId, companyName and phoneNumber blanked the customer's emailAddress, street address and suburb (the customerType and leadSource links survived). Read the customer first (`ascora_get_customers_customer_by_id`) and send it back whole with your changes. `customerType` and `leadSource` are created by NAME if they do not exist.
ascora_post_enquiryWRITECreate an enquiry (a lead) in Ascora from a contact's details and a description of the work wanted. POST /Enquiry. Answers {"entityId": "<uuid>", "success": true}, where entityId is the new enquiry's id -- the id to hand to `ascora_post_attachments_by_entitytype_by_entityid` or `ascora_post_notes_note` with entityType "Enquiry".
ascora_post_jobs_jobWRITECreate a job against a customer's site. POST /Jobs/Job. `siteCustomer` is the only REQUIRED field; job type, pricing method, billing customer and address all fall back to Ascora defaults. To create a job FROM a quote, mark the quote WON with `ascora_post_quotes_updatestatus` instead -- that carries the quote's items across, which this does not.
ascora_post_notes_noteWRITEAdd a note to an enquiry, job, quotation, invoice or customer. POST /Notes/Note. Note the SMALLER entity list than attachments: notes support Enquiry, Job, Quotation, Invoice and Customer only. Answers {"entityId": "<uuid>", "success": true} where entityId is the new NOTE's id.
ascora_post_quotes_addkitsWRITEAdd kits -- packaged combinations of supplies and labour -- to an existing quotation or one of its sections. POST /Quotes/AddKits. The quote number is the FULL number including any section/stage numbering. An unmatched id is silently accepted, as on the supplies form.
ascora_post_quotes_addlabouritemsWRITEAdd labour items to a quotation, by labour role and number of hours. POST /Quotes/AddLabourItems. `QuantityAdded` is HOURS and may be fractional. Measured 2026-09-19: an unmatched role id answers 200 {"success": true} and an empty list answers 200, while a bad quote number answers the documented QUOTE-NOT-FOUND envelope -- and this account's only labour role, whose hourlyRateExTax is 0.0000, answers HTTP 500 {"message": "An error has occurred."}. That 500 is Ascora-side and specific to that role's data; it is not a fault in the request.
ascora_post_quotes_addquotesectionsWRITEAdd child sections to a section-based quotation, in display order. POST /Quotes/AddQuoteSections. The parent quote's pricing method must already be SECTIONS. Measured 2026-09-19 against a TIME-AND-MATERIALS quote: 200 with {"success": false, "message": "Sections cannot be added to a non-section Quotation. Pricing Method is currently TIME-AND-MATERIALS", "code": "INVALID-PRICING-METHOD"}. Create the sections first, then add items to them by their child quote numbers.
ascora_post_quotes_addsuppliesWRITEAdd price-list supplies to an existing quotation, or to one of its sections. POST /Quotes/AddSupplies. The quote number is the FULL number including any section/stage numbering -- Q1234.01 addresses section 1. Measured 2026-09-19: an id that matches no supply is silently accepted (200 {"success": true}) rather than refused, so read the quote back to confirm what landed.
ascora_post_quotes_addwriteinsWRITEAdd write-in items to a quotation -- one-off lines that do not exist in any price list. POST /Quotes/AddWriteIns. Ascora files them under the account's default Write In category automatically.
ascora_post_quotes_clearquoteitemsWRITERemove all supplies, all kits and/or all labour from a quotation. DESTRUCTIVE: the removal is not per-item and cannot be undone through this API. POST /Quotes/ClearQuoteItems. Each flag clears an entire class of item. It applies to the quote number given ONLY -- on a section-based quote, pass the full child number (Q1234.01) to clear that section rather than the parent.
ascora_post_quotes_deletequoteWRITEDelete a quotation, or one section of one. DESTRUCTIVE and not reversible through this API. POST /Quotes/DeleteQuote. ONLY a quote whose status is In Progress can be deleted -- anything else answers 200 with {"success": false, "message": "Quotation (<n>) is not In Progress and so cannot be deleted", "code": "INVALID-STATUS"}. Deleting a SECTION recalculates the parent's totals and RENUMBERS the remaining sections, so section numbers held from an earlier call go stale.
ascora_post_quotes_quoteWRITECreate a quotation, optionally with its sections, supplies, kits, labour and write-ins in the same request. POST /Quotes/Quote. `siteCustomer` is the only REQUIRED field; everything else falls back to an Ascora default (the customer's own site address, the default job type, the site's billing customer, Time-and-Materials pricing). For a section-based quote, `pricingMethod` MUST be SECTIONS and the items go on `childQuotes` -- items at the parent level are ignored. Measured 2026-09-19: SECTIONS with no `childQuotes` answers 200 with {"success": false, "message": "Object reference not set to an instance of an object.", "code": "ERROR"} AND still consumes a quote number, so pass `childQuotes` whenever `pricingMethod` is SECTIONS.
ascora_post_quotes_updatestatusWRITEMove a quotation to a new status -- in progress, sent to customer, lost or won. POST /Quotes/UpdateStatus. Marking a quote WON CREATES THE RELATED JOB and returns its job id in the success message, so this is not only a status change. Note the two vocabularies Ascora uses for quote status and do not mix them: the integer on a quote RESPONSE is 0 In Progress, 1 Lost, 2 Converted to Job, 3 Sent to Client, while the string this call accepts is IN-PROGRESS, SENT-TO-CUSTOMER, LOST or WON.
ascora_post_supplierinvoices_markinvoicesassentWRITEMark supplier invoices as having been sent to your accounting package, so they stop appearing in the to-be-sent searches. POST /SupplierInvoices/MarkInvoicesAsSent. Answers a BARE INTEGER -- the number marked -- rather than a JSON object (measured 2026-09-19: an empty id list answers 200 with the body `0`). Do not confuse this with `ascora_post_accounting_markinvoicesassent`, which marks CUSTOMER invoices.
ascora_post_supplierinvoices_supplierinvoiceWRITECreate a supplier invoice or credit note with its line items. POST /SupplierInvoices/SupplierInvoice. `type` chooses which: INVOICE creates a supplier invoice, CREDIT creates a credit note. Attachments go on afterwards -- take `supplierInvoiceId` from the response and call `ascora_post_attachments_by_entitytype_by_entityid` with entityType "SupplierInvoice".
ascora_post_suppliers_supplierWRITECreate a supplier, or update an existing one by id. POST /Suppliers/Supplier. Supplying `supplierId` updates that supplier; omitting it creates a new one. ⚠ AN UPDATE REPLACES THE WHOLE RECORD, IT DOES NOT MERGE: every field omitted from the request is CLEARED. Measured 2026-09-19 -- an update sending only supplierId, name and phone blanked the supplier's businessNumber, email and address. Read the supplier first (`ascora_get_suppliers_supplier_by_id`) and send it back whole with your changes.
ascora_post_webhooksWRITESubscribe a URL to an Ascora event, so Ascora calls it when the event fires. POST /WebHooks. Answers {"subscriptionId": "<uuid>"} -- and NOTHING ELSE, with no `success` key (measured 2026-09-19). Keep that id: it is the only way to unsubscribe, and Ascora publishes no endpoint that lists existing subscriptions. An unrecognised event name answers HTTP 200 {"message": "Unknown Web Hook Event provided <name>"} with no subscriptionId; the connector turns that into a 400 `webhook_not_created` refusal rather than handing it back as a result (measured 2026-09-19).
Often connected alongside
Put Ascora behind one governed endpoint.
Same permissions, same audit trail, whatever else you connect next.