Skip to main content

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 code is 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:

  1. The new name is added beside the old one, and both work.
  2. The old one is marked as deprecated in the reference.
  3. The change goes in the log on this page, and we email every member who holds a working API key.
  4. 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_url on 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.js have 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 /mcp now offers one tool for each operation, named by its operation id, in place of tradesender_describe, tradesender_read and tradesender_write. A tool takes the operation's parameters by name and its request body as body. 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 has id, amount_cents, status, source, membership_id, refunded_at, failed_at, failure_reason and reference.
  • A refund's status is succeeded from the moment it is made, and failed when Stripe could not get the money back to the customer or the refund was cancelled. failed_at and failure_reason are set only for one that failed.
  • Its source says how it was made: app from TradeSender, through the business's Stripe account; manual for a payment recorded by hand, refunded as a record with no money moved; stripe outside TradeSender, in the Stripe dashboard or by Stripe. membership_id is the member who made it, and null for one made in Stripe. reference is the last 8 characters of Stripe's id for the refund, and null for one recorded by hand.
  • refunds is a history. refunded_cents is still the figure to count with: it is what the payment's refunds that succeeded add up to, and 0 for a payment that failed. Refunds made before today are added to refunds later, once. Until a payment's are, it can have refunded_cents above 0 and no refunds.

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 void or written_off with 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, updateVisitSeries and moveCalendarDay tell 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, updateVisitSeries and deleteVisitSeries tell 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_cents can now change on a payment recorded with createInvoicePayment or createCustomerPayment. Until now only a payment made online could be refunded. A program that keeps its own copy of a payment must read refunded_cents again, and paid_cents, balance_due_cents, payment_status and paid_at on its invoice follow it, as they do for a payment made online.
  • Every payment has a new field, online: true for a payment that went through the business's Stripe account, paid online by the customer or collected by Direct Debit, and false for one recorded with createInvoicePayment or createCustomerPayment.
  • listCustomerMessages and listMessages can return a new kind, refund: the email that tells a customer a payment of theirs was refunded. It carries the membership_id of whoever made the refund. Filtering listMessages by kind takes 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) and invoice_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_ups and visit_reminders, one for each of the emails the workspace sends by itself. Each is default (follow the workspace's switch), on (send it even while the switch is off) or off. createCustomer, updateCustomer, importCustomers and createEnquiryCustomer take them. A customer set on counts from that moment: nothing already overdue or awaiting an answer is chased that day.
  • automatic_emails on a customer is deprecated, and kept. It now stands for the three fields above: it reads false only when all three are off. Writing false sets all three to off; writing true sets all three to default when they were all off, and otherwise changes nothing. Sent with any of the three, it is ignored. Move to the three fields: automatic_emails keeps 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. createInvoice and updateInvoice take it. It is the one field updateInvoice changes on an issued invoice: send it with lock_version alone. Anything else on an issued invoice is refused with 409 invalid_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 is null for 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 by listCustomerMessages as invoice_reminder with the caller's membership_id, counts towards invoice_reminder_limit, and the next reminder that goes by itself waits invoice_reminder_gap_days from 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 409 invoice_reminder_already_sent. Managers only.
  • An invoice_reminder in listCustomerMessages may now carry a membership_id: one somebody sent with sendInvoiceReminder.

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 a reason. It is settled and never received: it is not a payment and is not in paid_cents. The invoice's balance_due_cents becomes 0 and it is no longer overdue, reminded, offered for payment online or collected by Direct Debit. A deposit invoice cannot be written off (409 invoice_is_deposit), nor one with a payment on its way or in dispute (409 invoice_has_settlements). Managers only.
  • A new operation, undoInvoiceWriteOff, puts what was written off back as left to pay. Until it is undone, createInvoicePayment and createCustomerPayment refuse a payment for the invoice with 409 invoice_written_off.
  • payment_status has a new value, written_off. A program that reads payment_status must expect it.
  • An invoice has four new fields: written_off_cents (0 unless written off), written_off_at, write_off_reason and written_off_by_membership_id. balance_due_cents is now the total less paid_cents and written_off_cents. paid_at stays null for 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 as listCustomerMessages. Filter by kind, by automatic (true for those nobody sent by hand), and by from and to, days in the workspace's time zone. Managers only.

10 October 2026

Customers and properties

  • createCustomer takes a new optional field, property: the customer's first property, with the fields createCustomerProperty takes. Both are saved, or neither is. When the property is refused, each error's source.pointer names its field under /data/attributes/property. It answers with two new fields, contacts and properties, beside everything it returned before.
  • The property that createEnquiryCustomer takes has the same fields as createCustomerProperty, so it can carry billing_name and billing_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. Send dry_run: true first: 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 422 too_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, or null when it has not been measured. createCustomerProperty, updateCustomerProperty and the property of createEnquiryCustomer take it. A negative size is refused with 422.
  • A property and the account each have two new read-only fields, latitude and longitude: the middle of the postcode, as numbers. They are looked up shortly after the address is saved and are null until then. They stay null for a postcode that is unknown, in Northern Ireland or outside the United Kingdom.

Quotes

  • sendQuote takes 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 createAssessmentQuote now has a valid_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. createQuote and updateQuote take it. It is 0 unless you set it, is refused with 422 when it is more than the quote's total_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 acceptQuote or 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 has kind deposit and the quote's quote_id, is dated and due the day the quote was accepted, and is otherwise an invoice like any other. acceptQuote emails 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_month and season_to_month (1 to 12, both or neither) are new optional fields on a series, on the series a visit carries, and on createVisitSeries, updateVisitSeries and the repeat of createJob. A season in which no visit would fall is refused with 422 season_has_no_visits.
  • createJob's repeat takes off_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_id and series become 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 billing has a new value, monthly ("At month end"). A program that reads billing must expect it. Such a job is priced and invoiced exactly as a per_visit job is, and createJobInvoice works 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_visit and monthly. Any other change of billing is still refused once a visit has been invoiced.
  • createJob and updateJob answer 409 invalid_transition when they would give a job another currency than 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 with membership_id. Show meta.attribution wherever you show the positions.
  • A new operation, moveCalendarDay, moves every visit still scheduled on 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 in skipped. With notify_customers it emails each customer one note listing their moved visits, listed by listCustomerMessages under the kind rescheduled; 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 every monthly job with completed visits up to a date (the last day of last month unless through says 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 as allocations says. It writes one payment on each invoice, all with the same reference. createInvoicePayment is 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 payments by itself.
  • A payment has two new fields. status is succeeded for a payment recorded with createInvoicePayment or createCustomerPayment; one made online is pending until the money clears, failed if it does not or if a dispute is lost, and disputed while the customer's bank decides. refunded_cents is how much of it has been refunded.
  • An invoice's paid_cents counts only payments that have succeeded, less their refunded_cents. A program that adds up payments itself 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. collecting is true while a payment made online is on its way, and collecting_cents is how much. Such an invoice is not overdue.
  • createCustomerPayment never puts money on an invoice that is collecting, which would be paid twice: allocations that name one are refused with 422 allocations_mismatch. Without allocations it also passes over an invoice that a Direct Debit is going to collect (one with a collect_on, described below), so the most it records is what the customer's other invoices come to, and more is refused with 422 payment_exceeds_balance. allocations may still name an invoice with a collect_on, for a customer who has paid it some other way.
  • An invoice's paid_at goes back to null when a refund, a failed payment or a dispute leaves it with a balance again.
  • voidInvoice now answers 409 invoice_has_settlements only while a payment holds money: one that succeeded with something left after refunds, is pending or is disputed. An invoice whose payments all failed or were refunded in full can be voided.
  • voidInvoice on 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 409 invoice_has_settlements when a customer has just paid on one and the payment has yet to be recorded, and 422 billing_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 the status of its Stripe account (onboarding, active, restricted) and the capabilities that are switched on. Its invoices can be paid online while capabilities has card_payments.
  • An invoice has three new fields. kind is deposit for such an invoice and standard for every other. deposit_invoice_id and deposit_deduction_cents are set on the invoice for the job when its quote's deposit was invoiced: the one invoice of a job billed on_completion, always, and for a per_visit or monthly job the first invoice that comes to at least the deposit. It is the same however that invoice is made: createJobInvoice, createMonthEndInvoices, or createInvoice and updateInvoice with the job's job_id. The deposit invoice's amount before tax comes off before tax, beside discount_cents, so that invoice is for the balance. total_cents is now subtotal_cents less discount_cents less deposit_deduction_cents, plus tax_cents. A program that works a total out from the lines must take the deduction off too; one that reads total_cents need do nothing.
  • On an invoice that takes a deposit off, tax_cents is 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's tax_cents is 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 read tax_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_cents and total_cents on a draft quote, a job or a draft invoice with a discount_cents can 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_on is the day the customer's bank is expected to be debited when the invoice is going to be collected by itself, and null otherwise. collectable says whether collectInvoice would start a collection now. direct_debit_failed is true when the last collection tried for it failed, or could not be started, and something is still left to pay. An invoice with a collect_on is not overdue, and its public_url no 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 a failed payment, with why in its notes. It answers 402 plan_limit_reached on a plan without Direct Debit collection, 409 invoice_not_collectable when the invoice is not collectable, and 409 invoice_has_settlements when 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 listCustomerMessages they have the kinds invoice_reminder, quote_follow_up and visit_reminder, with a null membership_id.
  • A customer has a new field, automatic_emails. It is true unless you set it otherwise on createCustomer or updateCustomer; false leaves that customer out of all three of those emails.
  • A visit has a new field, reminder_sent_at. It is null until the customer has been emailed a reminder of that visit.
  • reminder_sent_at on a visit is cleared when moveCalendarDay moves 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 by listCustomerMessages under the kind on_my_way, and counts towards the 100 emails a day. A second one for the same visit within 5 minutes is refused with 409 on_my_way_already_sent. Managers, and a technician assigned to the visit.
  • listCustomerMessages has a new kind, direct_debit_failed: the email a customer is sent when a Direct Debit for their invoice fails. It has a null membership_id.

Limits, errors and plans

  • Retry-After on 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 Accept has text/html and 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_limits has a new field, automation. It is 0 when the plan does not include automatic emails and Direct Debit collection, and null when it does.
  • An Idempotency-Key now 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 409 idempotency_key_reused and 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 payments as a pending payment of method direct_debit. Collection needs a plan whose plan_limits.automation is null, and a Stripe account whose capabilities has bacs_debit_payments.

9 October 2026

  • An enquiry has two new fields. kind is request when one of your customers asked for work from their customer portal, and enquiry otherwise. requested_date is the day a request asked for.
  • A request has its customer_id from the start and is new until somebody picks it up. createEnquiryCustomer picks it up: it creates nobody, and the request then reads converted.

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}/sites moved to /customers/{customer_id}/properties, and site_id became property_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.