Magento 2 REST API checkout payment gateway redirect

The fact that this endpoint defines result of the call as single int instead of at least object with order id and extension attribute is awful and probably won't change. We will see what the Magento team will come up for 2.3 when they will introduce their own PWA and hopefully create new endpoint with better support for 3rd party extensions. Right now however there are 2 possibilities to do:

Assuming you are making a request to /V1/guest-carts/:cartId/payment-information or /V1/carts/mine/payment-information you can:

  1. If your payment gateway requires GET redirect simply hook after Magento\Checkout\Api\PaymentInformationManagementInterface::savePaymentInformationAndPlaceOrder() and set redirect url according to your needs. Magento should recognize redirect header.

  2. If your redirect need to be actually a POST request you need to override definition of the endpoint and provide your own interface which will declare more reasonable result which you will be able to process in the browser. This however will require to make sure native Magento classes are also covered. If you are working on the project for single client this might work.

Create your own module, ie. Vendor_CheckoutExt as separate composer module or in app/code/Vendor/CheckoutExt. Make sure to add Magento_Checkout to sequence tag in module.xml so your definition will be read after magento one.

In etc/webapi.xml put a definition like:

<route url="/V1/carts/mine/payment-information" method="POST">
    <service class="Vendor\CheckoutExt\Api\PaymentInformationManagementInterface" method="savePaymentInformationAndPlaceOrder"/>
    <resources>
        <resource ref="self" />
    </resources>
    <data>
        <parameter name="cartId" force="true">%cart_id%</parameter>
    </data>
</route>

Create an interface Vendor\CheckoutExt\Api\PaymentInformationManagementInterface to look like

namespace Vendor\CheckoutExt\Api;

use Magento\Checkout\Api\PaymentInformationManagementInterface as MagentoPaymentInformationManagementInterface;

interface PaymentInformationManagementInterface extends MagentoPaymentInformationManagementInterface
{

    /**
     * Set payment information and place order for a specified cart.
     *
     * @param int $cartId
     * @param \Magento\Quote\Api\Data\PaymentInterface $paymentMethod
     * @param \Magento\Quote\Api\Data\AddressInterface|null $billingAddress
     * @throws \Magento\Framework\Exception\CouldNotSaveException
     * @return \Vendor\CheckoutExt\Api\Data\ResultInterface place order result data.
     */
    public function savePaymentInformationAndPlaceOrder(
        $cartId,
        \Magento\Quote\Api\Data\PaymentInterface $paymentMethod,
        \Magento\Quote\Api\Data\AddressInterface $billingAddress = null
    );
}

Now create interface for the response

namespace Vendor\CheckoutExt\Api\Data;

interface ResultInterface
{
    /**
     * @param string $orderId
     * @return Vendor\CheckoutExt\Api\Data\ResultInterface 
     */
    public function setOrderId($orderId);

    /**
     * @return string
     */
    public function getOrderId();

    /**
     * Retrieve existing extension attributes object or create a new one.
     *
     * @return Vendor\CheckoutExt\Api\Data\ResultInterface 
     */
    public function getExtensionAttributes();

    /**
     * Set an extension attributes object.
     *
     * @param Vendor\CheckoutExt\Api\Data\ResultExtensionInterface $extensionAttributes
     * @return $this
     */
    public function setExtensionAttributes(
        Vendor\CheckoutExt\Api\Data\ResultExtensionInterface $extensionAttributes
    );

}

We have interfaces ready so rest api module will be able to use it to prepare output. Now we need to implement them somehow to actually make the request work. Here there are 2 possibilities, you can either simply make a preference for the original one and prepare output using plugin or implement it itself. Let's go with the first option, in etc/di.xml or better in etc/webapi/di.xml define preference

<preference for="Vendor\CheckoutExt\Api\PaymentInformationManagementInterface" type="\Magento\Checkout\Model\PaymentInformationManagement" />

This will work because our interface extends native Magento one and we didn't change the function definition, only what should be returned from the function. But Magento class returns simple integer and just because we defined other output Magento won't generate it. We need to to this. So first let's implement class that we will use in the response

<preference for="Vendor\CheckoutExt\Api\Data\ResultInterface" type="Vendor\CheckoutExt\Model\Data\OrderResponse" />

namespace Vendor\CheckoutExt\Model\Data;

use Vendor\CheckoutExt\Api\Data\ResultInterface;
use Vendor\CheckoutExt\Api\Data\ResultExtensionInterface;
use Magento\Framework\Model\AbstractExtensibleModel;

class Result extends AbstractExtensibleModel implements ResultInterface
{
    /**
     * @param string $orderId
     * @return \Vendor\CheckoutExt\Api\Data\ResultInterface
     */
    public function setOrderId($orderId)
    {
        return $this->setData(self::ORDER_ID, $orderId);
    }

    /**
     * @return string
     */
    public function getOrderId()
    {
        return $this->_getData(self::ORDER_ID);
    }

    /**
     * @param string $incrementId
     * @return \Vendor\CheckoutExt\Api\Data\ResultInterface
     */
    public function setOrderRealId($incrementId)
    {
        return $this->setData(self::ORDER_REAL_ID, $incrementId);
    }

    /**
     * @return string
     */
    public function getOrderRealId()
    {
        return $this->_getData(self::ORDER_REAL_ID);
    }

    /**
     * @return \Vendor\CheckoutExt\Api\Data\ResultExtensionInterface
     */
    public function getExtensionAttributes()
    {
        $extensionAttributes = $this->_getExtensionAttributes();
        if (!$extensionAttributes) {
            /** @var ResultExtensionInterface $extensionAttributes */
            $extensionAttributes = $this->extensionAttributesFactory->create(ResultInterface::class);
        }

        return $extensionAttributes;
    }

    /**
     * @param \Vendor\CheckoutExt\Api\Data\ResultExtensionInterface $extensionAttributes
     * @return \Vendor\CheckoutExt\Api\Data\ResultInterface
     */
    public function setExtensionAttributes(ResultExtensionInterface $extensionAttributes)
    {
        return $this->_setExtensionAttributes($extensionAttributes);
    }
}

And last piece of the puzzle is the plugin which will hook after original place order method is called to change returned integer into object we need. Again in di.xml define

<type name="Magento\Checkout\Api\PaymentInformationManagementInterface">
    <plugin name="vendorCheckoutExtSaveOrderResultPlugin" type="Vendor\CheckoutExt\Plugin\Checkout\PaymentInformationManagement" sortOrder="1" />
</type>

And the plugin code

namespace Vendor\CheckoutExt\Plugin\Checkout;

use Vendor\CheckoutExt\Api\Data\ResultInterface;
use Vendor\CheckoutExt\Api\Data\ResultInterfaceFactory;
use Magento\Checkout\Api\PaymentInformationManagementInterface;

class PaymentInformationManagement
{
    /** @var ResultFactory */
    private $resultFactory;

    /**
     * @param ResultInterfaceFactory $resultFactory
     */
    public function __construct(ResultInterfaceFactory $resultFactory)
    {
        $this->resultFactory = $resultFactory;
    }

    /**
     * @param PaymentInformationManagementInterface $subject
     * @param int $orderId
     * @return ResultInterface
     */
    public function afterSavePaymentInformationAndPlaceOrder(
        PaymentInformationManagementInterface $subject,
        $orderId
    ) {
        /** @var ResultInterface $obj */
        $obj = $this->resultFactory->create();
        $obj->setOrderId($orderId);

        return $obj;
    }
}

So now the result is the object that implements extension_attributes. In your custom payment api module you can define similar plugin, just make sure sortOrder there is higher than the above one so you already get Vendor\CheckoutExt\Api\Data\ResultInterface object as an argument.

In payment module create file etc/extension_attributes.xml with

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Api/etc/extension_attributes.xsd">
    <extension_attributes for="Vendor\ChecoutExt\Api\Data\ResultInterface">
        <attribute code="my_payment_gateway_data" type="Vendor\CustomPayment\Api\Data\RedirectInterface" />
    </extension_attributes>
</config>

Create interface and its implementation according to your needs and in plugin

namespace Vendor\CustomPayment\Plugin\Checkout;

use Vendor\CustomPayment\Api\Data\RedirectInterface;
use Vendor\CustomPayment\Api\Data\RedirectInterfaceFactory;
use Vendor\CheckoutExt\Api\Data\ResultInterface;
use Magento\Checkout\Api\PaymentInformationManagementInterface;

class PaymentInformationManagement
{
    /** @var RedirectInterfaceFactory */
    private $redirectFactory;

    /**
     * @param RedirectInterfaceFactory $redirectFactory
     */
    public function __construct(RedirectInterfaceFactory $redirectFactory)
    {
        $this->redirectFactory = $redirectFactory;
    }

    /**
     * @param PaymentInformationManagementInterface $subject
     * @param ResultInterface $orderId
     * @return ResultInterface
     */
    public function afterSavePaymentInformationAndPlaceOrder(
        PaymentInformationManagementInterface $subject,
        $orderId
    ) {
        /** @var ResultInterface $obj */
        $redirect = $this->redirectFactory->create();

        $extensionAttributes = $orderId->getExtensionAttributes();
        $extensionAttributes->setMyPaymentGatewayData($redirect);
        $orderId->setExtensionAttributes($extensionAttributes);

        return $orderId;
    }
}

Now the response is proper json with extension_attributes in it which you can check and hook into. There might be some other elements to cover as magento by default redirect to success page, so you need to make sure you disable it when preparing your redirect. Also similar overwrites need to be done for classes handling guest checkout.