> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aikeedo.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Checkout flows

> Choose how an Aikeedo payment gateway takes the user through payment: a hosted redirect, an embedded form on your own page, or an offline token.

`purchase()` decides the shape of your checkout. There are three workable flows, and the right one depends on what your provider offers.

## Flow 1: Hosted redirect

The provider hosts the payment page. This is the simplest flow and the safest, since card data never touches the installation.

```php theme={null}
public function purchase(OrderEntity $order): UriInterface|PurchaseToken|string
{
    $session = $this->client->post('/checkout-sessions', [
        'amount' => $order->getTotalPrice()->value,
        'reference' => (string) $order->getId()->getValue(),
        'success_url' => $this->helper->generateReturnUrl($order, self::LOOKUP_KEY),
        'cancel_url' => $this->helper->generateCancelUrl($order, self::LOOKUP_KEY),
    ]);

    $order->initiatePayment(
        new PaymentGateway(self::LOOKUP_KEY),
        new ExternalId($session->id),
    );

    return new Uri($session->url);
}
```

Returning a `UriInterface` makes the browser follow it. When the user comes back, `completePurchase()` verifies the payment.

## Flow 2: Your own checkout page

Use this when the provider needs a client-side SDK, or when you must collect something extra, such as a bank choice.

Return a **relative** URI, which resolves against the site root:

```php theme={null}
return new Uri('app/billing/orders/' . $order->getId()->getValue() . '/demo-pay');
```

Then serve that page from your plugin, extending the billing view base class:

```php src/CheckoutView.php theme={null}
<?php

declare(strict_types=1);

namespace Acme\DemoPay;

use Billing\Application\Commands\ReadOrderCommand;
use Easy\Http\Message\RequestMethod;
use Easy\Router\Attributes\Route;
use Presentation\RequestHandlers\App\Billing\BillingView;
use Presentation\Resources\Api\OrderResource;
use Presentation\Response\RedirectResponse;
use Presentation\Response\ViewResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Shared\Infrastructure\CommandBus\Dispatcher;

#[Route(path: '/orders/[uuid:id]/demo-pay', method: RequestMethod::GET)]
class CheckoutView extends BillingView implements RequestHandlerInterface
{
    public function __construct(
        private Dispatcher $dispatcher,
    ) {}

    public function handle(ServerRequestInterface $request): ResponseInterface
    {
        $order = $this->dispatcher->dispatch(
            new ReadOrderCommand($request->getAttribute('id'))
        );

        if ($order->isPaid()) {
            return new RedirectResponse(
                '/app/billing/orders/' . $order->getId()->getValue() . '/receipt'
            );
        }

        return new ViewResponse('@demo-pay/checkout.twig', [
            'order' => new OrderResource($order),
            'public_key' => $this->publicKey,
        ]);
    }
}
```

`BillingView` puts the route under `/app/billing` and requires an authenticated user, so the page sits inside the app with the normal chrome.

The page loads the provider's script and, on success, sends the user to the callback URL:

```twig templates/checkout.twig theme={null}
{% extends "/layouts/main.twig" %}

{% block template %}
	<div id="demo-pay-element"></div>
{% endblock %}

{% block scripts %}
	{{ parent() }}
	<script src="https://js.demopay.test/v1.js"></script>
	<script>
		DemoPay.mount('#demo-pay-element', {
			key: '{{ public_key }}',
			amount: {{ order.total }},
			onSuccess: (payment) => {
				window.location.href =
					'/payment-callback/{{ order.id }}/demo-pay?payment_id=' + payment.id;
			},
		});
	</script>
{% endblock %}
```

If you need a server-side step before redirecting, such as creating a mandate from a form, add a POST endpoint under the same base class and redirect from there.

## Flow 3: Offline or token-based

Methods that settle outside Aikeedo, such as a bank transfer, return a `PurchaseToken`. The user goes to the receipt page, which shows your instructions, and an administrator marks the order paid later.

```php theme={null}
$token = Uuid::uuid4()->toString();

$order->initiatePayment(
    new PaymentGateway(self::LOOKUP_KEY),
    new ExternalId($token),
);

return new PurchaseToken($token);
```

Implement `OfflinePaymentGatewayInterface` so the method is listed with the manual options rather than as a branded button.

<Note>
  The generic checkout buttons don't do anything special with the token beyond sending the user to the receipt. Only the built-in card form consumes a token client-side.
</Note>

## Flow 4: Charge immediately

If the payment is already complete when `purchase()` runs, for example because you charged a saved payment method, return the provider reference as a plain `string`. Aikeedo pays and fulfils the order right away, without a callback.

```php theme={null}
$charge = $this->client->post('/charges', [...]);

return (string) $charge->id;
```

Only do this when the charge is genuinely settled. Returning a reference for a pending payment grants credits that may never be paid for.

## Where the reference lives

`completePurchase()` must return the provider reference, because it becomes the order's external ID and, for recurring plans, the subscription's. There are two ways to get it:

| Approach                        | How                                                                                                                                                  | When to use                                                                 |
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| **Store it before redirecting** | `$order->initiatePayment(new PaymentGateway(KEY), new ExternalId($id))` in `purchase()`, then read `$order->getExternalId()` in `completePurchase()` | The provider returns no useful parameters, or you don't trust what it sends |
| **Read it from the callback**   | `$params['payment_id'] ?? null` in `completePurchase()`                                                                                              | The provider appends its reference to the return URL                        |

Either way, verify against the provider before returning.

```php theme={null}
public function completePurchase(OrderEntity $order, array $params = []): string
{
    $id = $params['payment_id'] ?? (string) $order->getExternalId()->value;

    if (!$id) {
        throw new PaymentException('Missing payment reference');
    }

    $payment = $this->client->get('/payments/' . $id);

    if ($payment->reference !== (string) $order->getId()->getValue()) {
        throw new PaymentException('Payment reference mismatch');
    }

    if ($payment->status !== 'succeeded') {
        throw new PaymentException('Payment is not completed');
    }

    return (string) $payment->id;
}
```

<Warning>
  Compare the amount too when the provider lets the payer change it. An unchecked callback is an invitation to pay 1 unit for a 99 unit plan.
</Warning>

## Callback URLs

| Helper                             | Returns                                                           |
| ---------------------------------- | ----------------------------------------------------------------- |
| `generateReturnUrl($order, $key)`  | `/payment-callback/{orderId}/{key}` on the configured site domain |
| `generateCancelUrl($order, $key)`  | The billing page, for abandoned payments                          |
| `generateWebhookUrl($order, $key)` | `/webhooks/{key}`                                                 |

The callback accepts both `GET` and `POST`, and `completePurchase()` receives the query parameters merged with the parsed body, so a provider that posts its result works the same as one that redirects.

## Related

* [Build a gateway](/development/plugins/guides/payments/building-a-gateway)
* [Subscriptions and renewals](/development/plugins/guides/payments/subscriptions-and-renewals)
* [Webhooks](/development/plugins/guides/payments/webhooks)
