Changes and what we promise
What will not change under a program that uses the TradeSender API, how a breaking change is announced, and the log of changes so far.
The operations in the API reference are published. We keep them working.
What we promise
For every published operation:
- No operation, address, field, allowed value or error
codeis removed or renamed. - No field changes its type or its meaning.
- No request gains a required field.
What can change without notice
- New operations.
- New optional fields in a request.
- New fields in an answer.
- New allowed values for an existing field, such as a new status.
- New error codes.
- The wording of an error's
detail, which is for a person to read.
Write your program to ignore a field or a value it does not know, and to treat an error code it does not know by its status.
A breaking change
A change that would break the promise is made in steps:
- The new name is added beside the old one, and both work.
- The old one is marked as deprecated in the reference.
- The change goes in the log on this page, and we email every member who holds a working API key.
- The old one is removed no sooner than 90 days after that email.
Something that cannot be changed this way goes into a new version at /api/v2, and /api/v1 carries on beside it.
What the promise does not cover
- Operations the apps use that are not in the reference, such as sign-in, settings and the plan. They move with the apps.
- The addresses in emails to your customers. Use the
public_urlon a quote or an invoice to reach its page. - The tools an AI assistant sees at
/mcp. They are built from the same operations, but an assistant reads the tool list each time, so their descriptions are free to improve. - The enquiry form's address, its field names and
/enquiry.jshave a stronger promise of their own: they are pasted into other people's websites and do not change. See The enquiry form's address.
Change log
31 October 2026
Not part of the API
- The MCP server at
/mcpnow offers one tool for each operation, named by its operation id, in place oftradesender_describe,tradesender_readandtradesender_write. A tool takes the operation's parameters by name and its request body asbody. See The MCP server. AI apps read the tool list again by themselves. - An assistant is no longer offered
collectInvoice: a person collects an invoice by Direct Debit in the app. An API key can still call it.
21 October 2026
Refunds
- Every payment has a new field,
refunds: each refund of it, the oldest first, those that failed among them, and[]when there are none. A refund hasid,amount_cents,status,source,membership_id,refunded_at,failed_at,failure_reasonandreference. - A refund's
statusissucceededfrom the moment it is made, andfailedwhen Stripe could not get the money back to the customer or the refund was cancelled.failed_atandfailure_reasonare set only for one that failed. - Its
sourcesays how it was made:appfrom TradeSender, through the business's Stripe account;manualfor a payment recorded by hand, refunded as a record with no money moved;stripeoutside TradeSender, in the Stripe dashboard or by Stripe.membership_idis the member who made it, andnullfor one made in Stripe.referenceis the last 8 characters of Stripe's id for the refund, andnullfor one recorded by hand. refundsis a history.refunded_centsis still the figure to count with: it is what the payment's refunds thatsucceededadd up to, and 0 for a payment thatfailed. Refunds made before today are added torefundslater, once. Until a payment's are, it can haverefunded_centsabove 0 and norefunds.
Not part of the API
- A refund made in the app can now void the invoice, or write off what it leaves to pay, in the same step. Refunds are still made in the app only, so a program that keeps its own copy of an invoice may find it
voidorwritten_offwith its refund. The customer's refund email now says what is still to pay on the invoice.
19 October 2026
Not part of the API
- People in a workspace can now be notified on their phones. Nothing in the API changes, and a key cannot register a phone. Once a workspace uses notifications,
createJob,createJobVisit,createQuoteAssessment,updateVisit,createVisitSeries,updateVisitSeriesandmoveCalendarDaytell the people on a visit that they have been booked or that it has moved, whoever or whatever made the change.cancelVisit,cancelJob,completeJob,updateVisit,updateVisitSeriesanddeleteVisitSeriestell somebody that a visit of theirs today or tomorrow is no longer theirs. A change to a whole repeat tells each person once, and the member a key belongs to is not told about what the key does.
16 October 2026
Refunds
- Owners and office members can now refund a payment from an invoice in the app: one made online goes back through the business's Stripe account, and one recorded by hand is refunded as a record, with no money moving. Refunds are made in the app only. They are not in the API, and an assistant is not offered them.
refunded_centscan now change on a payment recorded withcreateInvoicePaymentorcreateCustomerPayment. Until now only a payment made online could be refunded. A program that keeps its own copy of a payment must readrefunded_centsagain, andpaid_cents,balance_due_cents,payment_statusandpaid_aton its invoice follow it, as they do for a payment made online.- Every payment has a new field,
online:truefor a payment that went through the business's Stripe account, paid online by the customer or collected by Direct Debit, andfalsefor one recorded withcreateInvoicePaymentorcreateCustomerPayment. listCustomerMessagesandlistMessagescan return a newkind,refund: the email that tells a customer a payment of theirs was refunded. It carries themembership_idof whoever made the refund. FilteringlistMessagesbykindtakes it too.
14 October 2026
Overdue reminders
- A workspace chooses when overdue invoices are reminded, with three new fields:
invoice_reminder_first_days(days after the due date before the first reminder, 1 to 60),invoice_reminder_gap_days(days between reminders, 1 to 90) andinvoice_reminder_limit(the most sent about one invoice, 1 to 10). They are 3, 14 and 3, which is what every workspace had until now. Changing one starts the count again, as turning reminders on does. - A customer has three new fields,
invoice_reminders,quote_follow_upsandvisit_reminders, one for each of the emails the workspace sends by itself. Each isdefault(follow the workspace's switch),on(send it even while the switch is off) oroff.createCustomer,updateCustomer,importCustomersandcreateEnquiryCustomertake them. A customer setoncounts from that moment: nothing already overdue or awaiting an answer is chased that day. automatic_emailson a customer is deprecated, and kept. It now stands for the three fields above: it readsfalseonly when all three areoff. Writingfalsesets all three tooff; writingtruesets all three todefaultwhen they were alloff, and otherwise changes nothing. Sent with any of the three, it is ignored. Move to the three fields:automatic_emailskeeps working until at least 12 January 2027.- An invoice has a new field,
invoice_reminders, with the same three values: its own setting, which beats its customer's.createInvoiceandupdateInvoicetake it. It is the one fieldupdateInvoicechanges on an issued invoice: send it withlock_versionalone. Anything else on an issued invoice is refused with 409invalid_transition, as before. - An invoice has a new read-only field,
reminders: how many reminders it has had (sent), the most that go by themselves (limit), the first day the next can go (next_on), and, when none will, why (reason). It isnullfor a draft, a void or archived invoice, and one with nothing left to pay. - A new operation,
sendInvoiceReminder, emails the customer the overdue reminder at once. It is listed bylistCustomerMessagesasinvoice_reminderwith the caller'smembership_id, counts towardsinvoice_reminder_limit, and the next reminder that goes by itself waitsinvoice_reminder_gap_daysfrom it. It counts towards the 100 emails a day. A second one for the same invoice on the same day, by anybody or by the workspace itself, is refused with 409invoice_reminder_already_sent. Managers only. - An
invoice_reminderinlistCustomerMessagesmay now carry amembership_id: one somebody sent withsendInvoiceReminder.
Writing off an unpaid balance
- A new operation,
writeOffInvoice, writes off what is left to pay on an issued invoice when the customer will not pay it, with areason. It is settled and never received: it is not a payment and is not inpaid_cents. The invoice'sbalance_due_centsbecomes 0 and it is no longer overdue, reminded, offered for payment online or collected by Direct Debit. A deposit invoice cannot be written off (409invoice_is_deposit), nor one with a payment on its way or in dispute (409invoice_has_settlements). Managers only. - A new operation,
undoInvoiceWriteOff, puts what was written off back as left to pay. Until it is undone,createInvoicePaymentandcreateCustomerPaymentrefuse a payment for the invoice with 409invoice_written_off. payment_statushas a new value,written_off. A program that readspayment_statusmust expect it.- An invoice has four new fields:
written_off_cents(0 unless written off),written_off_at,write_off_reasonandwritten_off_by_membership_id.balance_due_centsis now the total lesspaid_centsandwritten_off_cents.paid_atstaysnullfor an invoice written off. - The customer's page for an invoice has a new field,
written_off_cents, so that the page adds up. It never says why.
Emails to customers
- A new operation,
listMessages, lists the emails sent to every customer of the workspace, newest first, with the same rows aslistCustomerMessages. Filter bykind, byautomatic(true for those nobody sent by hand), and byfromandto, days in the workspace's time zone. Managers only.
10 October 2026
Customers and properties
createCustomertakes a new optional field,property: the customer's first property, with the fieldscreateCustomerPropertytakes. Both are saved, or neither is. When the property is refused, each error'ssource.pointernames its field under/data/attributes/property. It answers with two new fields,contactsandproperties, beside everything it returned before.- The
propertythatcreateEnquiryCustomertakes has the same fields ascreateCustomerProperty, so it can carrybilling_nameandbilling_address. - A new operation,
importCustomers, brings in up to 100 customers and their properties in one request, from a spreadsheet or another product's export. Senddry_run: truefirst: it answers what the same rows will do and saves nothing. A row for a customer who is already there creates nobody and fills in only what they have blank; a row that cannot be saved is skipped with its reason, and the rest are imported. More than 100 rows is refused with 422too_many_rows. Managers only. - A property has a new field,
lawn_area_m2: the size of its lawn in square metres, a decimal string such as"240.0"with up to two decimal places, ornullwhen it has not been measured.createCustomerProperty,updateCustomerPropertyand thepropertyofcreateEnquiryCustomertake it. A negative size is refused with 422. - A property and the account each have two new read-only fields,
latitudeandlongitude: the middle of the postcode, as numbers. They are looked up shortly after the address is saved and arenulluntil then. They staynullfor a postcode that is unknown, in Northern Ireland or outside the United Kingdom.
Quotes
sendQuotetakes a new optional field,valid_until: a new last day for the quote, set before it is sent. With it a quote that has expired can be sent again, and the customer can then accept it. It must be today or later, and is ignored once the quote is accepted.- A quote made with
createAssessmentQuotenow has avalid_until, 30 days from the day it is made. It used to have none. - A quote has a new field,
deposit_cents: a deposit asked for when the quote is accepted, tax included.createQuoteandupdateQuotetake it. It is0unless you set it, is refused with 422 when it is more than the quote'stotal_cents, and like the rest of a quote can be changed only while the quote is a draft. - Accepting a quote that asks for a deposit, with
acceptQuoteor by the customer on their page, now issues an invoice for the deposit in the same step, so a program that lists invoices will see one it did not create. It haskinddepositand the quote'squote_id, is dated and due the day the quote was accepted, and is otherwise an invoice like any other.acceptQuoteemails nobody. A quote with no deposit behaves as before.
Jobs, repeating visits and the calendar
- A repeating visit can run for only part of the year.
season_from_monthandseason_to_month(1 to 12, both or neither) are new optional fields on a series, on theseriesa visit carries, and oncreateVisitSeries,updateVisitSeriesand therepeatofcreateJob. A season in which no visit would fall is refused with 422season_has_no_visits. createJob'srepeattakesoff_season(interval,unit): a second series on the same job for the months the season leaves out.- A job has a new field,
repeats: its series that still have dates to book. - A series that ends on a date is now removed overnight once every visit up to that date is booked and none is still scheduled or in progress. Its visits stay, and their
visit_series_idandseriesbecome null. Finishing the last outstanding visit of such a job completes the job, as it does for a job that does not repeat. - A job's
billinghas a new value,monthly("At month end"). A program that readsbillingmust expect it. Such a job is priced and invoiced exactly as aper_visitjob is, andcreateJobInvoiceworks on it at any time; the difference is that its completed visits are meant to wait until their month is over. - A job with an invoiced visit may now move between
per_visitandmonthly. Any other change ofbillingis still refused once a visit has been invoiced. createJobandupdateJobanswer 409invalid_transitionwhen they would give a job anothercurrencythan the one its quote's deposit was invoiced in. A job from a quote with no deposit invoice, or one that has been voided, is not affected.- A new operation,
getDayRoute, returns the visits on one day that are scheduled or in progress, in a sensible order to drive them, with the distance in a straight line from each stop to the next. It changes no visit. A technician gets their own visits; an owner or office member gets everyone's, or one person's withmembership_id. Showmeta.attributionwherever you show the positions. - A new operation,
moveCalendarDay, moves every visit stillscheduledon one day to another day, for everyone or for one member, keeping each visit's time of day, length, crew and repeat. Visits that are in progress, completed or cancelled stay and are listed inskipped. Withnotify_customersit emails each customer one note listing their moved visits, listed bylistCustomerMessagesunder the kindrescheduled; a workspace can send 300 of these in 24 hours, and they are not counted towards the 100 a day. All or nothing. Managers only.
Invoices and payments
- A new operation,
createMonthEndInvoices, raises and issues one invoice for everymonthlyjob with completed visits up to a date (the last day of last month unlessthroughsays otherwise), and emails each to its customer. It answers with the invoices, how many were emailed, and those whose customer has no email address. A run takes up to 300 jobs and is all or nothing. Its emails are not counted towards the 100 a day. Managers only. - A new operation,
createCustomerPayment, records one payment across several of a customer's invoices: to the oldest due first, or asallocationssays. It writes one payment on each invoice, all with the samereference.createInvoicePaymentis unchanged. Managers only. - A business can now take payment online, through a Stripe account of its own that an owner connects in the app's settings. Connecting is not part of the API. A payment its customer makes online appears in the invoice's
paymentsby itself. - A payment has two new fields.
statusissucceededfor a payment recorded withcreateInvoicePaymentorcreateCustomerPayment; one made online ispendinguntil the money clears,failedif it does not or if a dispute is lost, anddisputedwhile the customer's bank decides.refunded_centsis how much of it has been refunded. - An invoice's
paid_centscounts only payments that havesucceeded, less theirrefunded_cents. A program that adds uppaymentsitself must do the same. Until now every payment had succeeded and none was refunded, so nothing already recorded reads differently. - An invoice has two new fields.
collectingis true while a payment made online is on its way, andcollecting_centsis how much. Such an invoice is notoverdue. createCustomerPaymentnever puts money on an invoice that iscollecting, which would be paid twice:allocationsthat name one are refused with 422allocations_mismatch. Withoutallocationsit also passes over an invoice that a Direct Debit is going to collect (one with acollect_on, described below), so the most it records is what the customer's other invoices come to, and more is refused with 422payment_exceeds_balance.allocationsmay still name an invoice with acollect_on, for a customer who has paid it some other way.- An invoice's
paid_atgoes back tonullwhen a refund, a failed payment or a dispute leaves it with a balance again. voidInvoicenow answers 409invoice_has_settlementsonly while a payment holds money: one that succeeded with something left after refunds, ispendingor isdisputed. An invoice whose payments allfailedor were refunded in full can be voided.voidInvoiceon an invoice a customer could pay online now also closes any payment page still open for it, so that nobody can pay a void invoice. It answers 409invoice_has_settlementswhen a customer has just paid on one and the payment has yet to be recorded, and 422billing_failed, changing nothing, when Stripe cannot be reached to close a page. Try again in either case.- A workspace has a new field,
stripe_connect:null, or thestatusof its Stripe account (onboarding,active,restricted) and thecapabilitiesthat are switched on. Its invoices can be paid online whilecapabilitieshascard_payments. - An invoice has three new fields.
kindisdepositfor such an invoice andstandardfor every other.deposit_invoice_idanddeposit_deduction_centsare set on the invoice for the job when its quote's deposit was invoiced: the one invoice of a job billedon_completion, always, and for aper_visitormonthlyjob the first invoice that comes to at least the deposit. It is the same however that invoice is made:createJobInvoice,createMonthEndInvoices, orcreateInvoiceandupdateInvoicewith the job'sjob_id. The deposit invoice's amount before tax comes off before tax, besidediscount_cents, so that invoice is for the balance.total_centsis nowsubtotal_centslessdiscount_centslessdeposit_deduction_cents, plustax_cents. A program that works a total out from the lines must take the deduction off too; one that readstotal_centsneed do nothing. - On an invoice that takes a deposit off,
tax_centsis the tax its lines carry after the discount, less the tax on the deposit invoice. So the two invoices come to the total and the tax that one invoice for the work would have, to the penny. Each line'stax_centsis what is left of that line's tax, and the lines add up to the invoice's. A program that works tax out as a rate on what is left can be a penny or two away on such an invoice, so readtax_cents. - With something off before tax, each line is taxed on its share of what is left, and a share of exactly half a penny now rounds up. It used to round down. So
tax_centsandtotal_centson a draft quote, a job or a draft invoice with adiscount_centscan be a penny or two higher the next time it is saved: two lines of £15.00 at 20% with 5p off now carry 600 of tax, where they carried 598. A quote that has been sent and an invoice that has been issued keep the sums they had, and are no longer worked out again when they are saved. - An invoice has three new fields.
collect_onis the day the customer's bank is expected to be debited when the invoice is going to be collected by itself, andnullotherwise.collectablesays whethercollectInvoicewould start a collection now.direct_debit_failedis true when the last collection tried for it failed, or could not be started, and something is still left to pay. An invoice with acollect_onis notoverdue, and itspublic_urlno longer offers payment online or bank details: a second payment would be taken as well. - A new operation,
collectInvoice, collects what is left to pay on an invoice by the customer's Direct Debit at once: sooner than its day, or again after a collection has failed. It takes money from a customer's bank account. Nothing is ever tried again by itself. A collection that cannot be started is recorded on the invoice as afailedpayment, with why in itsnotes. It answers 402plan_limit_reachedon a plan without Direct Debit collection, 409invoice_not_collectablewhen the invoice is notcollectable, and 409invoice_has_settlementswhen the customer has just paid it online. Managers only.
Emails to customers
- A new operation,
listCustomerMessages, lists the emails TradeSender has sent a customer, newest first: quotes, invoices, their portal sign-in link and the three emails below. Emails sent before it existed are not listed. Managers only. - A workspace can now send three emails by itself: a reminder when an invoice is overdue, a follow-up when a quote has had no answer, and a reminder the day before a visit. Each is switched on in the app's settings. In
listCustomerMessagesthey have the kindsinvoice_reminder,quote_follow_upandvisit_reminder, with a nullmembership_id. - A customer has a new field,
automatic_emails. It istrueunless you set it otherwise oncreateCustomerorupdateCustomer;falseleaves that customer out of all three of those emails. - A visit has a new field,
reminder_sent_at. It isnulluntil the customer has been emailed a reminder of that visit. reminder_sent_aton a visit is cleared whenmoveCalendarDaymoves the visit, so it is reminded the evening before its new day. It is set instead when the customer was emailed about a new day that is today, tomorrow or the day after.- A new operation,
sendVisitOnMyWay, emails a visit's customer that the caller is on the way and roughly when they will arrive (minutes: 15, 30, 45 or 60). It is listed bylistCustomerMessagesunder the kindon_my_way, and counts towards the 100 emails a day. A second one for the same visit within 5 minutes is refused with 409on_my_way_already_sent. Managers, and a technician assigned to the visit. listCustomerMessageshas a new kind,direct_debit_failed: the email a customer is sent when a Direct Debit for their invoice fails. It has a nullmembership_id.
Limits, errors and plans
Retry-Afteron a 429 is now the length of the limit that was reached, in seconds. It is still 60 for the limit on requests, and is 86400 once the 100 emails a day have been sent. It used to be 60 for every limit.- The enquiry form's address answers a plain HTML form differently when it refuses the post. A request whose
Accepthastext/htmland no JSON type now gets a small HTML page that says what went wrong, with the same status and headers. Every other request gets the same JSON as before. - A workspace's
plan_limitshas a new field,automation. It is0when the plan does not include automatic emails and Direct Debit collection, andnullwhen it does. - An
Idempotency-Keynow belongs to the way it was first sent as well as to the member: with an API key, from a signed-in app, or by an AI assistant. The same key sent another way is refused with 409idempotency_key_reusedand is never answered from what was kept. A retry sent the same way is answered as before.
Not part of the API
- Owners and office members can now record in the app what a service, and each line of a quote or a job, costs the business. This is not part of the API. A key is never sent a cost, and one you send is ignored without an error, so nothing you read or write has changed.
- A business's customers can now agree a Bacs Direct Debit with it, in their customer portal. Setting one up is not part of the API. An invoice issued once their bank has confirmed it is collected from the customer's bank by itself, on or just after its due date, and the collection appears in the invoice's
paymentsas apendingpayment of methoddirect_debit. Collection needs a plan whoseplan_limits.automationisnull, and a Stripe account whosecapabilitieshasbacs_debit_payments.
9 October 2026
- An enquiry has two new fields.
kindisrequestwhen one of your customers asked for work from their customer portal, andenquiryotherwise.requested_dateis the day a request asked for. - A request has its
customer_idfrom the start and isnewuntil somebody picks it up.createEnquiryCustomerpicks it up: it creates nobody, and the request then readsconverted.
8 October 2026
- The API is public. Members can make API keys in Settings, then API keys.
- The reference is published. The promise on this page starts today.
7 October 2026
- A customer's addresses became properties. The operations under
/customers/{customer_id}/sitesmoved to/customers/{customer_id}/properties, andsite_idbecameproperty_id. This happened the day before the API was published and is the last change of its kind.
Checked against the app on 31 October 2026. Still stuck? Ask usAsk us.