Ad – 728×90
🖥️ POS Development

Odoo POS Payment Integration – Custom Payment Methods

Odoo POS supports cash and card payments out of the box. To integrate a custom payment terminal (e.g., a gift card system, a loyalty wallet, or a third-party payment provider), you implement a PaymentInterface subclass in JavaScript and back it with a pos.payment.method record in Python.

⏱️ 25 min 🎯 Advanced 📅 Updated 2026
What you'll learn:
  • How pos.payment.method works
  • The PaymentInterface JavaScript class
  • Sending and receiving payment terminal messages
  • Handling payment success, failure, and cancellation
  • Split payment across multiple methods

pos.payment.method

pos.payment.method is the database model for a payment method. Each method is linked to a journal (bank or cash) in accounting. Fields relevant to custom integrations:

  • name — displayed on the PaymentScreen button
  • journal_id — linked accounting journal
  • use_payment_terminal — selection field; your module registers its terminal type here
  • pos_config_ids — which POS configs this method is available on

Register your terminal type by extending the use_payment_terminal selection field:

Python
from odoo import models, fields


class PosPaymentMethod(models.Model):
    _inherit = 'pos.payment.method'

    def _get_payment_terminal_selection(self):
        return super()._get_payment_terminal_selection() + [
            ('my_terminal', 'My Custom Terminal'),
        ]

PaymentInterface – JavaScript Class

Every payment terminal integration requires a JavaScript class that extends PaymentInterface. Register it so Odoo POS loads it when the payment method's use_payment_terminal matches your key:

JavaScript
/** @odoo-module **/
import { PaymentInterface } from '@point_of_sale/app/payment/payment_interface';
import { register_payment_method } from '@point_of_sale/app/store/pos_store';

export class PaymentMyTerminal extends PaymentInterface {

    /**
     * Called when the cashier clicks "Send" on the payment screen.
     * Start the terminal transaction here.
     */
    async sendPaymentRequest(cid) {
        const paymentLine = this.pos.get_order().get_paymentline(cid);
        const amount = paymentLine.get_amount();

        // Call Python method on the server to initiate the terminal
        const result = await this.env.services.orm.call(
            'pos.payment.method',
            'initiate_terminal_payment',
            [this.payment_method.id, amount],
        );

        if (result.success) {
            paymentLine.set_payment_status('waitingCard');
            return true;  // waiting for terminal callback
        }
        paymentLine.set_payment_status('retry');
        return false;
    }

    /**
     * Called when the cashier clicks "Cancel" during payment.
     */
    async sendPaymentCancel(order, cid) {
        await this.env.services.orm.call(
            'pos.payment.method',
            'cancel_terminal_payment',
            [this.payment_method.id],
        );
        return true;
    }
}

register_payment_method('my_terminal', PaymentMyTerminal);

Polling for Terminal Response

Payment terminals are asynchronous — the cashier sends a request and waits for a response. Poll the server for the terminal status:

JavaScript
export class PaymentMyTerminal extends PaymentInterface {

    async sendPaymentRequest(cid) {
        const paymentLine = this.pos.get_order().get_paymentline(cid);
        paymentLine.set_payment_status('waitingCard');

        // Poll every 2 seconds for terminal result
        this._pollingInterval = setInterval(async () => {
            const status = await this.env.services.orm.call(
                'pos.payment.method',
                'get_terminal_status',
                [this.payment_method.id],
            );

            if (status.state === 'approved') {
                clearInterval(this._pollingInterval);
                paymentLine.set_payment_status('done');
                paymentLine.transaction_id = status.transaction_id;
                this.pos.get_order().validate_order();
            } else if (status.state === 'declined') {
                clearInterval(this._pollingInterval);
                paymentLine.set_payment_status('retry');
            }
        }, 2000);

        return true;
    }
}
Ad – 728×90

Split Payments

Odoo POS supports split payments natively — a customer can pay part in cash and part by card. The PaymentScreen shows multiple payment lines when the cashier adds multiple payment methods. No special code is needed; Odoo's validate_order() handles posting multiple payment lines to accounting.

Each payment line generates a separate pos.payment record and a corresponding journal entry. Your integration only needs to handle the payment lines assigned to your terminal.

To access the order's payment lines in JavaScript:

JavaScript
const order = this.pos.get_order();
const paymentLines = order.get_paymentlines();
const myLines = paymentLines.filter(
    line => line.payment_method.use_payment_terminal === 'my_terminal'
);
const totalMyTerminal = myLines.reduce(
    (sum, line) => sum + line.get_amount(), 0
);
Key takeaways:
  • Register your terminal type by extending _get_payment_terminal_selection() on pos.payment.method
  • Implement sendPaymentRequest() and sendPaymentCancel() in your PaymentInterface subclass
  • Register the class with register_payment_method('your_key', YourClass)
  • Split payments are handled automatically — no extra code needed

Frequently Asked Questions

Does Odoo have built-in support for card payment terminals?

Yes — Odoo has built-in integrations for Adyen, Stripe Terminal, and Ingenico via separate modules (pos_adyen, pos_stripe, etc.). These follow the same PaymentInterface pattern. If your terminal isn't supported natively, implement a custom integration using the pattern shown here.

How do I handle a terminal timeout?

Set a maximum polling duration. If the terminal doesn't respond within (e.g.) 60 seconds, clear the interval, call sendPaymentCancel() to abort the terminal transaction, and set the payment line status to 'retry'. Always provide a way for the cashier to retry or cancel.

Where is pos.payment stored in accounting?

When the session is closed, Odoo creates a bank/cash statement for each payment method with a journal. Each pos.payment becomes a line on the statement. Reconciliation against the journal's account happens when the session closes and calls action_pos_session_closing_control().