Friday, September 25, 2026
1 change · saas-19.4
Enhancements to existing features
Xendit payments now use Xendit's newer hosted payment flow, keeping checkout available as older Xendit APIs are phased out. Customers are redirected to Xendit for all payment methods, existing saved cards remain usable, and businesses receive a one-time reminder to update webhook settings so payment statuses keep syncing.
Original PR description
The legacy v2/invoices and credit_card_charges endpoints for new payments are deprecated by Xendit. This migrates checkout and card sessions to the /sessions Payment Sessions API redirect flow and…
The legacy v2/invoices and credit_card_charges endpoints for new payments are
deprecated by Xendit. This migrates checkout and card sessions to the
/sessions Payment Sessions API redirect flow and charges v3 tokens through
/v3/payment_requests, while still accepting pre-existing v2 tokens (there is
no documented migration path for them) through the legacy
credit_card_charges endpoint.
Session creation:
- endpoint: v2/invoices -> sessions; response invoice_url -> payment_link_url
- external_id -> reference_id, with session_type PAY or SAVE (for
validation operations) and mode PAYMENT_LINK
- validation operations use the company's own currency instead of the
generic fallback's arbitrary pick, as Xendit's payment channels are
activated per country and an unrelated currency can be rejected
- customer.given_names -> customer.individual_detail.given_names and
surname, split via payment_utils.split_partner_name and sanitized to
alphanumeric (API constraint)
- customer reference_id is made unique per session creation attempt
(customer{partner_id}{random suffix}) as Xendit rejects reused
references and never reuses existing customers
- success_redirect_url/failure_redirect_url ->
success_return_url/cancel_return_url
- payment_methods -> allowed_payment_channels
- addresses removed (not supported by the sessions API)
- country derived from partner.country_id.code, with company fallback
- mobile_number sanitized to E.164 (+digits only, no spaces)
- card payments with tokenization set allow_save_payment_method=FORCED
and card_on_file_type=CUSTOMER_UNSCHEDULED
- removed the inline card form and its direct flow (payment_form.js), the
Xendit SDK and the /payment/xendit/payment route: all methods now
redirect to the Xendit-hosted payment link; the passthrough
_get_redirect_form_view override is dropped as well, and so are the
processing values only the inline form used (rounded_amount, currency,
access_token)
- the now-unneeded xendit_public_key credential is no longer required nor
shown in the provider form; the field itself is kept, as dropping a field
is not allowed in stable
- the session id is saved as soon as the session is created, rather than
waiting for the webhook, so it is available even if the customer returns
first
Tokenized payments:
- charge v3 tokens (prefixed 'pt-') through /v3/payment_requests
(api-version 2024-11-11) instead of /credit_card_charges, using
payment_token_id; channel_code is omitted as it is rejected when paying
with a token
- tokens saved before the migration to the v3 Payment Tokens API aren't
prefixed 'pt-' and aren't accepted by /v3/payment_requests; since there is
no documented way to migrate them, they are still charged through
/credit_card_charges with is_recurring set, same as before this migration
- card_on_file_type is CUSTOMER_UNSCHEDULED for a customer actively paying
with a saved card, or MERCHANT_UNSCHEDULED for an unattended charge
(operation 'offline', e.g. a subscription renewal), to reduce the odds
of a 3DS challenge without forcing skip_three_ds: that flag requires a
dashboard feature most merchants haven't activated, and isn't needed
for card-on-file charges since the card network's own MIT exemption
rules already cover them
- channel_properties requires success_return_url/failure_return_url even
for these off-session charges; built without an access token as the
charge may run outside of a request context (e.g. from a cron)
- if a v3 token charge unexpectedly still requires 3DS authentication
(status REQUIRES_ACTION), a customer-present charge exposes the
authentication URL Xendit returns as the pending_authentication_url
processing value, and the frontend navigates the top window to it
directly rather than submitting a form, since Xendit's page requires the
query string carrying the API key, only accepts GET and refuses to render
inside a frame;
an unattended charge has no cardholder to redirect, so it is set to
error instead of being left pending indefinitely
- create the payment token from payment_token_id; the masked card number
is not included in payment/payment request notifications, but is
included in `payment_token.activation` webhook notifications, which are
handled directly to avoid the extra request; otherwise, it is fetched
with a GET on /v3/payment_tokens/{id} (_xendit_make_request now
supports the GET method)
Status sync on return:
- a customer returning from checkout, or from a 3DS challenge for a v3
token charge, is checked directly against Xendit (via a new
_xendit_sync_from_provider) before falling back to the previous
pending-by-default behavior, in case the webhook is delayed or dropped
- the token charge's success return URL now carries the reference and an
access token when issued from a live request, so a customer returning
from a 3DS challenge can also be checked this way; a charge with no
request context (e.g. a cron renewal) is unaffected, as there is no
cardholder to redirect in that case
- the return controller now also matches `pending` transactions, since a
v3 token charge is already in that state by the time of the 3DS return
Webhook handling:
- unwrap the {event, data} envelope sent for session events
- look up transactions by exact reference_id match (or external_id for
legacy credit_card_charges notifications), falling back to stripping the
random suffix Xendit appends to payment request references
- store payment_session_id or payment_request_id as provider reference
- extend the status mapping: pending (PENDING, ACTIVE, REQUIRES_ACTION),
done (SUCCEEDED, PAID, CAPTURED, COMPLETED),
cancel (CANCELLED, EXPIRED, CANCELED)
Upgrade compatibility:
- the inline_form template is emptied rather than removed: the provider
record is noupdate, so existing databases keep inline_form_view_id
pointing to it, and removing the view would abort the module update
(ondelete='restrict')
- _should_build_inline_form is overridden to never build the inline form:
until the module is updated, the view keeps its old card inputs in
database, which would otherwise still be displayed and whose values would
be silently discarded, as the payment goes through the redirect flow
- bump the module version so partners upgrading notice the change
Webhook migration notice:
- Xendit replaced the single "Invoices paid" webhook field with separate v3
event groups (Payment tokens v3 / Payment requests v3). Databases that had
Xendit configured before this change stop receiving payment and card token
status updates until the Xendit Dashboard is updated with the new fields
- remind admins of this by hooking into the daily autovacuum cron instead of
a dedicated one or an upgrade-triggered migration script: SaaS/.sh don't
force module upgrades, so a migration script would only catch the
providers, companies, and admins that existed at the exact moment of the
upgrade, and a dedicated ir.cron record wouldn't exist on already-installed
databases until then either. @api.autovacuum methods are discovered
directly from the Python class, so they run immediately everywhere, keep
covering providers and admins added later, and can be removed later by
deleting the method, with no leftover cron record to clean up through a
migration
- track whether admins were already notified by checking for an existing
pending activity instead of adding a stored field on payment.provider: a
new column wouldn't exist either on databases where the module isn't
upgraded, and the ORM would error on any domain filtering on it. This
means marking the reminder done re-schedules it on the next run, since
there's no way to tell "resolved" apart from "dismissed" without a field;
that's accepted, as the underlying condition (the webhook still needs
reconfiguring) hasn't actually changed either way
- notify base.group_system, account.group_account_manager, and
sales_team.group_sale_manager rather than only Sales admins: those are the
groups actually granted write access to payment.provider and its Xendit
credential fields, or otherwise likely to own the provider's
configuration, and covering all three means databases using Xendit
without the Sales app installed (e.g. through Invoicing or Point of Sale)
still have someone to notify. A user in more than one of these groups is
only notified once
- the activity note links the "webhook configuration" and "Xendit Dashboard"
mentions inline to the Odoo documentation and to the Xendit Dashboard's
webhook settings, respectively, calling out the October 1, 2026 date after
which the old webhook field stops being honored. The cron itself stops
scheduling new activities after October 7, 2026, since there's nothing
left to prevent once Xendit has already cut over
Task-6373405
Forward-Port-Of: odoo/odoo#277626