Install
Works with Drupal: ^11.3 || ^12Using Composer to manage Drupal site dependencies
Alternative installation files
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
PaymentQueryhassubjects(a list ofPaymentSubject) andsubjectIds, which replacessubjectId.PaymentQuery::MAX_SUBJECTSis 100, andPaymentWire::MAX_IDS(100) is the most ids one API request may name.- Renamed, with no deprecation step:
RefusedException::inside()is nowfindInside(), andOnwardUrl::redirect()is nowbuildRedirect(). Code catching aDeletionRefusedExceptionor aChangeRefusedException, or building a gateway's onward redirect, has to follow. ApiFloodSubscriberis replaced by theDrupal\kessai_server\ApiLimitservice, andPaymentApiControllertakes 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.)