Language guides

WordPress without a plugin

Sell from any WordPress or WooCommerce site using core functions: a payment shortcode, a return check and a signed REST webhook.

11 min read

What you will build

No plugin is required. Two snippets — one in your child theme's functions.php and one MU plugin — give you a payment button, a confirmation page and a webhook endpoint at /wp-json/takaflow/v1/webhook.

Use it on a custom page, or hook the same calls into WooCommerce so orders complete automatically.

  • WordPress 6.x with HTTPS and the REST API enabled.
  • An API key and webhook secret from Developers in your workspace.
  • Edit access to wp-config.php and wp-content/mu-plugins/.

1. Store your keys

wp-config.php
// add above "That's all, stop editing"
define("TAKAFLOW_BASE", "https://your-domain.com");
define("TAKAFLOW_API_KEY", "tf_live_xxxxxxxxxxxxxxxx");
define("TAKAFLOW_WEBHOOK_SECRET", "whsec_xxxxxxxxxxxxxxxx");

2. A payment button shortcode

This creates a charge and renders a button. Drop [takaflow_pay amount="1250" label="Pay now"] on any page or post. Store the invoice number against the order so the return page and webhook can find it.

functions.php
function takaflow_pay_button($atts) {
    $atts = shortcode_atts(["amount" => "0", "label" => "Pay now"], $atts);
    $order_id = "WP-" . time();

    $res = wp_remote_post(TAKAFLOW_BASE . "/api/charge", [
        "timeout" => 20,
        "headers" => [
            "RT-TAKAFLOW-API-KEY" => TAKAFLOW_API_KEY,
            "Content-Type" => "application/json",
        ],
        "body" => wp_json_encode([
            "full_name" => wp_get_current_user()->display_name,
            "email" => wp_get_current_user()->user_email,
            "amount" => (float) $atts["amount"],
            "description" => "WordPress order " . $order_id,
            "metadata" => ["order_id" => $order_id],
            "redirect_url" => home_url("/payment-received/"),
        ]),
    ]);

    if (is_wp_error($res)) return "Payment is unavailable right now.";
    $charge = json_decode(wp_remote_retrieve_body($res), true);
    if (empty($charge["payment_url"])) return "Payment is unavailable right now.";

    update_option("takaflow_" . $charge["invoice_number"], $order_id);

    return '<a class="button" href="' . esc_url($charge["payment_url"]) . '">'
        . esc_html($atts["label"]) . " " . esc_html($charge["amount"]) . " BDT</a>";
}
add_shortcode("takaflow_pay", "takaflow_pay_button");

3. Confirm on the return page

Create a page at /payment-received/ and use a block template or a snippet that reads invoice_id from the query string, verifies it, and only then shows the receipt.

page-payment-received.php
$invoice = sanitize_text_field($_GET["invoice_id"] ?? "");

if ($invoice === "") {
    echo "Missing invoice reference.";
    return;
}

$res = wp_remote_post(TAKAFLOW_BASE . "/api/verify-payment", [
    "timeout" => 20,
    "headers" => [
        "RT-TAKAFLOW-API-KEY" => TAKAFLOW_API_KEY,
        "Content-Type" => "application/json",
    ],
    "body" => wp_json_encode(["invoice_id" => $invoice]),
]);

$state = json_decode(wp_remote_retrieve_body($res), true);

if (($state["payment_status"] ?? "") === "COMPLETED") {
    $order_id = $state["metadata"]["order_id"] ?? "";
    update_option("takaflow_order_" . $order_id, "paid");
    echo "Payment received. Thank you!";
} else {
    echo "We are still verifying this payment.";
}

4. Webhook endpoint

Drop this file in wp-content/mu-plugins/takaflow-webhook.php. It registers a REST route and verifies the signature on the raw request body before marking anything paid.

mu-plugins/takaflow-webhook.php
<?php
add_action("rest_api_init", function () {
    register_rest_route("takaflow/v1", "/webhook", [
        "methods" => "POST",
        "callback" => "__return_null",
        "permission_callback" => "__return_true",
    ]);
});

add_filter("rest_pre_dispatch", function ($result, $server, $request) {
    if ($request->get_route() !== "/takaflow/v1/webhook") return $result;

    $raw = $request->get_body();
    $expected = "sha256=" . hash_hmac("sha256", $raw, TAKAFLOW_WEBHOOK_SECRET);
    $received = $request->get_header("x-takaflow-signature") ?? "";

    if (!hash_equals($expected, $received)) {
        return new WP_Error("takaflow_signature", "Invalid signature", ["status" => 401]);
    }

    $event = json_decode($raw, true);
    if (($event["event"] ?? "") === "payment.completed") {
        $order_id = $event["metadata"]["order_id"] ?? "";
        update_option("takaflow_order_" . $order_id, "paid");
        // send email, reduce stock, grant access ...
    }

    return new WP_REST_Response(["ok" => true], 200);
}, 10, 3);

The webhook URL is https://your-site.com/wp-json/takaflow/v1/webhook. If your host blocks the REST API, add the route through a normal admin-post.php action instead, but keep the same signature check.

5. WooCommerce notes

  • Create the charge on woocommerce_checkout_order_processed and save invoice_number in the order meta.
  • On payment.completed, load the order with wc_get_order(metadata.order_id) and call $order->payment_complete($trx_id).
  • Keep WooCommerce's own gateway list untouched; the hosted checkout replaces the payment step.
  • Cache-bust the payment button page if you use a full-page cache — each click must create a fresh charge.

6. Go-live checklist

  • Constants live in wp-config.php, never in the theme repository.
  • Webhook URL answers 200 to a test delivery in Developers → Webhooks.
  • Return page verifies with the API instead of trusting the query string.
  • Order updates are idempotent on X-TakaFlow-Delivery.
  • A small live payment from a second phone completes the order end to end.