All integrations

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_subscriptionidWRITE

Remove 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.

api
ascora_get_accounting_getinvoicestosendREAD

Retrieve 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.

api
ascora_get_accounting_getpaymentstosendREAD

Retrieve 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.

api
ascora_get_customers_contact_by_idREAD

Retrieve 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.

api
ascora_get_customers_contactsREAD

Search contacts across all customers, with paging and `totalPages`/`totalRecords`. GET /Customers/Contacts. Each result carries the `customerId` the contact belongs to.

api
ascora_get_customers_customer_by_idREAD

Retrieve 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.

api
ascora_get_customers_customersREAD

Search 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.)

api
ascora_get_customers_getcontactsforcustomer_by_customeridREAD

List 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".

api
ascora_get_inventory_categoriesREAD

Search 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`).

api
ascora_get_inventory_kitsREAD

Search 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`.

api
ascora_get_inventory_suppliesREAD

Search 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`.

api
ascora_get_jobs_job_by_jobnumberREAD

Retrieve 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.

api
ascora_get_jobs_joblabour_by_jobnumberREAD

List 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).

api
ascora_get_jobs_jobsREAD

Search 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).

api
ascora_get_jobs_jobtimesheets_by_jobnumberREAD

List 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).

api
ascora_get_quotes_labourrolesREAD

List 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`.

api
ascora_get_quotes_quote_by_quotenumberREAD

Retrieve 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.

api
ascora_get_quotes_quotesREAD

Search 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.

api
ascora_get_quotes_standardsectionsREAD

List 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).

api
ascora_get_quotes_standardstagesREAD

List 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).

api
ascora_get_supplierinvoices_supplierinvoice_by_idREAD

Retrieve 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.

api
ascora_get_supplierinvoices_supplierinvoicesREAD

Search 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.

api
ascora_get_suppliers_supplier_by_idREAD

Retrieve 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.

api
ascora_get_suppliers_suppliersREAD

Search 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.

api
ascora_post_accounting_createpaymentWRITE

Record 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.

api
ascora_post_accounting_markinvoicesassentWRITE

Mark 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.

api
ascora_post_accounting_markpaymentsassentWRITE

Mark 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).

api
ascora_post_attachments_by_entitytype_by_entityidWRITE

Attach 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`.

api
ascora_post_customers_contactWRITE

Create 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.

api
ascora_post_customers_customerWRITE

Create 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.

api
ascora_post_enquiryWRITE

Create 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".

api
ascora_post_jobs_jobWRITE

Create 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.

api
ascora_post_notes_noteWRITE

Add 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.

api
ascora_post_quotes_addkitsWRITE

Add 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.

api
ascora_post_quotes_addlabouritemsWRITE

Add 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.

api
ascora_post_quotes_addquotesectionsWRITE

Add 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.

api
ascora_post_quotes_addsuppliesWRITE

Add 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.

api
ascora_post_quotes_addwriteinsWRITE

Add 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.

api
ascora_post_quotes_clearquoteitemsWRITE

Remove 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.

api
ascora_post_quotes_deletequoteWRITE

Delete 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.

api
ascora_post_quotes_quoteWRITE

Create 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.

api
ascora_post_quotes_updatestatusWRITE

Move 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.

api
ascora_post_supplierinvoices_markinvoicesassentWRITE

Mark 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.

api
ascora_post_supplierinvoices_supplierinvoiceWRITE

Create 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".

api
ascora_post_suppliers_supplierWRITE

Create 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.

api
ascora_post_webhooksWRITE

Subscribe 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).

api

Put Ascora behind one governed endpoint.

Same permissions, same audit trail, whatever else you connect next.