Install

Works with Drupal: ^11.3 || ^12

Using Composer to manage Drupal site dependencies

Alternative installation files

Download tar.gz 773.36 KB
MD5: e65084cb9fa7fcd66e2623e4400d43ee
SHA-1: da206bfbae23c32e520302112cf88d59446e4823
SHA-256: 8047b2c00c304fd8b76dd2f55f9aabe97185a01d207719f42b1bce7af80824e1
Download zip 1004.83 KB
MD5: 0563e04e473951164aec463d6cb944c5
SHA-1: 8075b9127eda8b17e3420809c7a90e1a44453bfa
SHA-256: a6f3267a57cc9d5d34493ff275aae1fd01e0ea1b160c6e9818368d47412b6f6f

Release notes

No schema change and no update hook: an alpha8 site upgrades in place. Deploy the code, rebuild caches (a service and a route changed), then re-import the French catalogs of kessai_engine, kessai_client and kessai_audit_trail with drush locale:import fr MODULE/translations/fr.po --type=not-customized --override=not-customized, one per catalog. Where payments are settled on another host, upgrade the site running kessai_server first, then its consumer sites. Both sides still speak wire version 3, so the handshake does not catch a mismatch. An alpha8 server ignores the new subject_ids field, so a consumer on this release asking it about a page of subjects would be answered with every payment of that subject type. The other way round, an alpha8 consumer is refused where it sends a field this release no longer reads, or more than 100 ids in one request.

A remote's access token is no longer sent to a new address

#3621987, for sites running kessai_client. A remote's OAuth access token was cached under the remote's id and reused until it expired. Editing a remote's address made the form check the new address before saving, and that check sent the token the old host had issued. Changing the client id or the secret key kept calling as the old consumer until the token expired, so the check recorded the old consumer's account. The check now asks for a fresh token and keeps none, and saving a remote forgets its cached token. A remote nobody changes keeps its token as before.

A currency is now checked, and a capture and a cancel of one hold no longer run at once

#3621988. Taking a payment, holding against a stored card and charging one now refuse a currency that is not three capital letters, such as EUR. Before, eur, an empty currency or € was stored and then refused by the provider with the payer at the checkout, and a code longer than three characters failed in the database, which the API answered with a 500. A capture and a cancel of the same payment used to take two different locks. A cancel arriving while a capture was with the provider could release the whole hold, so the payment recorded more money captured and released than it held and announced both kessai.captured and kessai.canceled. The two now share one lock. A subscriber to kessai.captured can still cancel the rest of the hold from inside the same request.

Reading what a consumer remembered about its payments costs a query, not an entity per row

#3621985. Every list of payments the API answers with, and every search by metadata, loaded each metadata row as an entity to read one or three strings from it, and wrote each row to the entity cache. Measured on MySQL with 3,000 payments and 9,000 rows, reading their metadata went from 397 ms and 130 MiB to 20 ms and 3 MiB, and a search by a value they all share from 309 ms to 191 ms. Matching names and values works as before on every database.

The API's request limit no longer runs on every page

#3621984, for sites running kessai_server. The limit was a kernel.request listener, so every request of the site built it, API or not. Each API endpoint now asks it itself, and nothing else pays for it. The limits, the 429 answer and the log line are unchanged. The endpoints that take a list of payment ids (/kessai-api/payments/read, /payments/delete and /payments/forget-tokens) now refuse more than 100 ids in one request, which a request limit could not bound. The client sends a longer list in several requests, so getPayments(), deletePayments() and deleteStoredTokens() still take any list. A handoff that is not true or false, and an operation that is not a string, are now refused with a 422. Before, the string "false" asked for a handoff link.

Every API endpoint refuses a field it does not read

#3621983, #3621982. A misspelled field used to be dropped and read as its default, and on these endpoints the defaults move money: a capture sent amounts instead of amount took the whole hold and closed the payment, and a refund returned the whole balance. Every endpoint that reads a body now answers 400, naming the fields it does not read and the ones it does. The endpoints that take a list of ids also refuse a body that is not a JSON object: they used to read it as an empty list and answer as though nothing had been named.

One call can ask about a whole page of subjects

#3621982. PaymentQuery takes subjects:, a list of up to 100 subjects of one type, so a listing asks once per page rather than once per row. On MySQL, a page of 50 costs 2 queries where 50 single lookups cost 100. An empty list is refused rather than read as no subject at all. Over the API the field is subject_ids, which replaces subject_id.

The remote form says what a remote's id is, and what disabling one does

#3621986, for sites running kessai_client. The form said a remote's id was "the gateway id a module names when it asks for a payment". A module names the account uuid the remote reaches; the id is only this site's machine name for it. The form also said a disabled remote's payments "are still settled". They are refused, and so are its host's notifications, as the remote documentation already said. The listing's "Gateway id" column is now "Machine name", and the payment accounts listing writes "UUID". French follows.

French

#3621980. The description of Kessai Audit Trail no longer leaves "Audit Trail" in English.

For developers

  • PaymentQuery has subjects (a list of PaymentSubject) and subjectIds, which replaces subjectId. PaymentQuery::MAX_SUBJECTS is 100, and PaymentWire::MAX_IDS (100) is the most ids one API request may name.
  • Renamed, with no deprecation step: RefusedException::inside() is now findInside(), and OnwardUrl::redirect() is now buildRedirect(). Code catching a DeletionRefusedException or a ChangeRefusedException, or building a gateway's onward redirect, has to follow.
  • ApiFloodSubscriber is replaced by the Drupal\kessai_server\ApiLimit service, and PaymentApiController takes it as a sixth constructor argument.
  • RemoteTokenProvider::requestToken() asks a remote for a fresh token and keeps nothing.
  • The audit trail subscriber now listens to every event PaymentEvents::getEventNames() lists, so an event added to the contract is recorded without being named twice.

AI-Generated: Yes (Claude Code was used to help draft these release notes. I reviewed them, and each behavior change was checked against the 1.0.0-alpha8 code.)

Created by: mably
Created on: 20 Oct 2026 at 19:28 UTC
Last updated: 20 Oct 2026 at 19:28 UTC
Bug fixes

Other releases