DesenvolvedorWordPress WhatsApp

Criar um gateway de pagamento no WooCommerce

Integrar um provedor de pagamento (PSP) ao WooCommerce é estender uma classe, implementar um punhado de métodos e — o que separa o amador do profissional — tratar a confirmação assíncrona com segurança. Veja o caminho completo, do registro da classe ao webhook idempotente.

Precisa de desenvolvimento sob medida?

Atualizado em 19/06/2026

A API de gateways do WooCommerce é elegante: para integrar qualquer provedor de pagamento — de um PSP nacional a uma solução própria —, você cria uma classe que estende WC_Payment_Gateway e a registra com um filtro. O WooCommerce cuida de exibir a opção no checkout, renderizar a tela de configuração no admin e conduzir o pedido pelo seu ciclo de status. O seu trabalho se concentra em dois pontos: descrever o método (campos de configuração) e implementar a lógica de cobrança em process_payment(). Este guia cobre o esqueleto completo e, principalmente, as partes que costumam ser feitas errado: a confirmação assíncrona, a idempotência e a segurança.

A anatomia de um gateway

Um gateway de pagamento no WooCommerce é composto por três peças que se encaixam:

  1. O registro: um add_filter( 'woocommerce_payment_gateways', … ) que adiciona o nome da sua classe ao array de gateways conhecidos.
  2. A classe: sua extensão de WC_Payment_Gateway, carregada depois que o WooCommerce existe, com a configuração e a lógica de pagamento.
  3. A confirmação: um endpoint (webhook/callback) que recebe a notificação do PSP e atualiza o pedido de forma confiável e idempotente.

Tudo isso vive em um plugin próprio — nunca no functions.php do tema, já que um gateway é funcionalidade que precisa sobreviver à troca de tema. E, como toda integração com o WooCommerce, depende de hooks tanto para se registrar quanto para receber callbacks.

Registrando a classe (no momento certo)

O ponto mais sutil do início é o tempo de carregamento. Como a sua classe estende WC_Payment_Gateway, ela só pode ser definida depois que essa classe-pai existir — ou seja, depois que o WooCommerce carregou. Por isso, prendemos tudo a plugins_loaded e protegemos com class_exists():

<?php
/**
 * Plugin Name: DW Gateway de Exemplo
 * Description: Esqueleto de gateway de pagamento para WooCommerce.
 */

defined( 'ABSPATH' ) || exit;

// 1) Carrega a classe SÓ quando o WooCommerce já existe.
add_action( 'plugins_loaded', 'dw_gateway_init', 11 );

function dw_gateway_init() {
    if ( ! class_exists( 'WC_Payment_Gateway' ) ) {
        return; // WooCommerce inativo: não faz nada (não quebra o site).
    }
    require_once __DIR__ . '/includes/class-dw-gateway.php';
}

// 2) Registra a classe no array de gateways do WooCommerce.
add_filter( 'woocommerce_payment_gateways', 'dw_gateway_register' );

function dw_gateway_register( $gateways ) {
    $gateways[] = 'DW_Gateway'; // o NOME da classe, não uma instância.
    return $gateways; // filter sempre retorna.
}

Repare em dois detalhes de especialista: o filtro adiciona uma string com o nome da classe (o WooCommerce instancia internamente, uma única vez), e o return $gateways; é obrigatório — esquecê-lo apaga todos os gateways da loja, um clássico de quem ainda confunde actions e filters.

A classe: construtor e configuração

O construtor define a identidade do gateway, carrega as configurações salvas e pendura o hook que persiste as opções do admin. Veja o esqueleto:

<?php
defined( 'ABSPATH' ) || exit;

class DW_Gateway extends WC_Payment_Gateway {

    public function __construct() {
        $this->id                 = 'dw_gateway';            // identificador único
        $this->has_fields         = false;                   // true se coletar dados no checkout
        $this->method_title       = 'DW Gateway';            // nome na tela de admin
        $this->method_description = 'Recebe pagamentos via PSP DW (exemplo).';
        $this->supports           = array( 'products', 'refunds' );

        // Carrega os campos de configuração e os valores salvos.
        $this->init_form_fields();
        $this->init_settings();

        // Valores exibidos ao cliente (vêm das configurações).
        $this->title       = $this->get_option( 'title' );
        $this->description = $this->get_option( 'description' );
        $this->enabled     = $this->get_option( 'enabled' );
        $this->testmode    = 'yes' === $this->get_option( 'testmode' );

        // Salva as opções quando o admin clica em "Salvar".
        add_action(
            'woocommerce_update_options_payment_gateways_' . $this->id,
            array( $this, 'process_admin_options' )
        );

        // Recebe o callback assíncrono do PSP (webhook). Gera a URL:
        // https://sua-loja/?wc-api=dw_gateway
        add_action( 'woocommerce_api_' . $this->id, array( $this, 'handle_webhook' ) );
    }
}

Pontos a destacar: $this->id tem que ser único e estável — ele entra no nome de vários hooks (como o de salvar opções e o do webhook). $this->supports declara o que o gateway sabe fazer ('refunds' habilita o estorno pelo admin, e exige implementar process_refund()). E process_admin_options() é um método pronto da classe-pai — você não o escreve, só o pendura no hook de salvamento.

Os campos de configuração: init_form_fields()

A tela de configuração do gateway (em WooCommerce › Ajustes › Pagamentos) é gerada automaticamente a partir do array de init_form_fields(), usando a Settings API do WooCommerce. Você descreve os campos; o WooCommerce renderiza, valida e salva:

<?php
public function init_form_fields() {
    $this->form_fields = array(
        'enabled' => array(
            'title'   => 'Ativar/Desativar',
            'type'    => 'checkbox',
            'label'   => 'Ativar o DW Gateway',
            'default' => 'no',
        ),
        'title' => array(
            'title'       => 'Título',
            'type'        => 'text',
            'description' => 'Nome exibido ao cliente no checkout.',
            'default'     => 'Pagamento via DW',
            'desc_tip'    => true,
        ),
        'description' => array(
            'title'   => 'Descrição',
            'type'    => 'textarea',
            'default' => 'Pague com segurança usando o DW Gateway.',
        ),
        'testmode' => array(
            'title'       => 'Modo de teste',
            'type'        => 'checkbox',
            'label'       => 'Usar o ambiente sandbox do PSP',
            'default'     => 'yes',
            'description' => 'Em produção, desative e use as chaves de produção.',
        ),
        'api_key' => array(
            'title' => 'API Key (produção)',
            'type'  => 'password', // não exibe a chave em texto puro
        ),
        'test_api_key' => array(
            'title' => 'API Key (sandbox)',
            'type'  => 'password',
        ),
        'webhook_secret' => array(
            'title'       => 'Segredo do webhook',
            'type'        => 'password',
            'description' => 'Usado para validar a assinatura (HMAC) das notificações do PSP.',
            'default'     => '',
        ),
    );
}

Você lê esses valores em qualquer ponto da classe com $this->get_option( 'api_key' ). O par modo de teste / produção não é detalhe cosmético: ele decide qual chave e qual URL do PSP usar, e jamais deve ser confundido em produção. Use type => 'password' para chaves, de modo que não fiquem visíveis em texto puro na tela.

O coração: process_payment( $order_id )

Quando o cliente finaliza a compra escolhendo o seu gateway, o WooCommerce chama process_payment() com o ID do pedido. Este método precisa: obter o pedido pela CRUD API, comunicar-se com o PSP e retornar um array indicando sucesso (com a URL de redirecionamento) ou falha. Veja uma implementação completa:

<?php
public function process_payment( $order_id ) {
    $order  = wc_get_order( $order_id );   // CRUD API, nunca get_post().
    $logger = wc_get_logger();

    if ( ! $order ) {
        wc_add_notice( 'Pedido inválido.', 'error' );
        return array( 'result' => 'failure', 'redirect' => '' );
    }

    // Seleciona a credencial/URL conforme o modo.
    $api_key = $this->testmode
        ? $this->get_option( 'test_api_key' )
        : $this->get_option( 'api_key' );

    // 1) Chama o PSP para criar a cobrança (exemplo com a HTTP API do WP).
    $response = wp_remote_post( 'https://api.psp-exemplo.com/charges', array(
        'timeout' => 30,
        'headers' => array(
            'Authorization' => 'Bearer ' . $api_key,
            'Content-Type'  => 'application/json',
        ),
        'body' => wp_json_encode( array(
            'amount'      => (int) round( $order->get_total() * 100 ), // centavos
            'currency'    => $order->get_currency(),
            'reference'   => $order->get_order_number(),
            'callback'    => WC()->api_request_url( $this->id ), // URL do webhook
        ) ),
    ) );

    // 2) Trata erro de comunicação.
    if ( is_wp_error( $response ) ) {
        $logger->error( 'Falha ao contatar o PSP: ' . $response->get_error_message(),
            array( 'source' => $this->id ) );
        wc_add_notice( 'Não foi possível iniciar o pagamento. Tente novamente.', 'error' );
        return array( 'result' => 'failure', 'redirect' => '' );
    }

    $data = json_decode( wp_remote_retrieve_body( $response ), true );

    // 3) Sucesso: marca o pedido e redireciona.
    if ( ! empty( $data['approved'] ) ) {
        // Guarda o ID da transação para conciliar com o webhook depois.
        $order->update_meta_data( '_dw_transaction_id', sanitize_text_field( $data['id'] ) );

        // payment_complete() já muda o status (processing/completed) e
        // reduz o estoque internamente; use-o em pagamento aprovado na hora.
        $order->payment_complete( $data['id'] );
        $order->add_order_note( 'Pagamento aprovado pelo DW Gateway. TX: ' . $data['id'] );
        $order->save();

        WC()->cart->empty_cart(); // limpa o carrinho.

        return array(
            'result'   => 'success',
            'redirect' => $this->get_return_url( $order ), // página de obrigado.
        );
    }

    // 4) Falha de pagamento: mantém o pedido e mostra o erro.
    $motivo = isset( $data['error'] ) ? sanitize_text_field( $data['error'] ) : 'recusado';
    $logger->notice( "Pagamento recusado (pedido {$order_id}): {$motivo}",
        array( 'source' => $this->id ) );
    wc_add_notice( 'Pagamento recusado: ' . $motivo, 'error' );
    return array( 'result' => 'failure', 'redirect' => '' );
}

Sobre estoque: $order->payment_complete() já cuida da redução de estoque internamente ao mudar o status. Se, em vez dele, você usar $order->update_status() para um fluxo "aguardando confirmação" (pagamento que só confirma depois, via webhook), aí sim você controla o estoque manualmente com wc_reduce_stock_levels( $order_id ) no momento certo — tipicamente quando o webhook confirma o pagamento.

Quando o gateway coleta dados: payment_fields e validate_fields

Se você definiu $this->has_fields = true (por exemplo, para um formulário de cartão próprio), precisa de dois métodos a mais. payment_fields() imprime o HTML que aparece sob a opção de pagamento no checkout, e validate_fields() valida o que o cliente digitou antes de process_payment() rodar:

<?php
public function payment_fields() {
    // Descrição configurável + seus campos. Sempre escape a saída.
    if ( $this->description ) {
        echo wpautop( wp_kses_post( $this->description ) );
    }
    echo '<p class="form-row">
            <label>CPF do titular <span class="required">*</span></label>
            <input type="text" name="dw_cpf" autocomplete="off">
          </p>';
}

public function validate_fields() {
    $cpf = isset( $_POST['dw_cpf'] ) ? sanitize_text_field( wp_unslash( $_POST['dw_cpf'] ) ) : '';
    if ( strlen( preg_replace( '/\D/', '', $cpf ) ) !== 11 ) {
        wc_add_notice( 'Informe um CPF válido.', 'error' );
        return false; // bloqueia o checkout.
    }
    return true;
}

Nunca trafegue ou armazene dados completos de cartão no seu servidor sem necessidade — isso joga você dentro do escopo pesado de PCI-DSS. O padrão moderno é tokenizar no navegador (a biblioteca JS do PSP transforma o cartão em um token) e enviar só o token ao seu process_payment(). Colete o mínimo possível de dado sensível.

A parte que separa o profissional: confirmação assíncrona

Aqui mora o erro mais grave de gateways amadores: confiar apenas no redirect. Em fluxos onde o cliente é enviado ao ambiente do PSP (ou em pagamentos assíncronos como Pix e boleto), o pagamento se confirma fora da requisição do checkout. O redirect de volta pode nunca acontecer — o cliente fecha o navegador, a rede cai, o app do banco não retorna. Se você marcou o pedido como pago só porque houve redirect, vai liberar pedidos não pagos; se só confia no redirect para confirmar, vai perder pagamentos legítimos.

A fonte de verdade é o webhook (também chamado de IPN/callback): o PSP envia, do servidor dele para o seu, uma notificação de mudança de status. Registramos o endpoint no construtor via woocommerce_api_{id}, que cria a URL ?wc-api=dw_gateway. O handler precisa fazer três coisas inegociáveis: validar a assinatura (provar que veio mesmo do PSP), garantir idempotência (não processar duas vezes) e responder com o código HTTP correto.

<?php
public function handle_webhook() {
    $logger  = wc_get_logger();
    $payload = file_get_contents( 'php://input' );
    $sig     = isset( $_SERVER['HTTP_X_PSP_SIGNATURE'] )
        ? wp_unslash( $_SERVER['HTTP_X_PSP_SIGNATURE'] ) : '';

    // 1) Validação de assinatura: HMAC do corpo com o segredo do webhook.
    $secret   = $this->get_option( 'webhook_secret' );
    $expected = hash_hmac( 'sha256', $payload, $secret );
    if ( ! hash_equals( $expected, (string) $sig ) ) {
        $logger->warning( 'Webhook com assinatura inválida.', array( 'source' => $this->id ) );
        status_header( 401 );
        exit;
    }

    $event    = json_decode( $payload, true );
    $order_id = isset( $event['reference'] ) ? absint( $event['reference'] ) : 0;
    $order    = $order_id ? wc_get_order( $order_id ) : false;

    if ( ! $order ) {
        status_header( 404 );
        exit;
    }

    // 2) Idempotência: se já está pago, apenas confirma o recebimento.
    if ( $order->has_status( array( 'processing', 'completed' ) ) ) {
        status_header( 200 );
        exit;
    }

    // 3) Aplica a mudança conforme o status enviado pelo PSP.
    if ( 'paid' === ( $event['status'] ?? '' ) ) {
        $order->payment_complete( sanitize_text_field( $event['id'] ?? '' ) );
        $order->add_order_note( 'Pagamento confirmado via webhook do PSP.' );
    } elseif ( 'failed' === ( $event['status'] ?? '' ) ) {
        $order->update_status( 'failed', 'Pagamento falhou (webhook).' );
    }

    status_header( 200 ); // 200 evita que o PSP fique reenviando.
    exit;
}

Três observações de quem já apanhou disso em produção: (1) compare assinaturas com hash_equals(), não com ==, para evitar ataques de timing; (2) responda 200 só quando processou (ou já tinha processado) — qualquer outro código faz o PSP reenviar, o que é bom, desde que o seu handler seja idempotente; (3) registre tudo com wc_get_logger() e um source próprio, porque webhook que falha em silêncio é o pesadelo de depurar.

Métodos e propriedades: quem faz o quê

Método / propriedadeResponsabilidade
$this->idIdentificador único do gateway; entra no nome de vários hooks.
$this->method_title / method_descriptionNome e descrição exibidos na tela de admin (lista de pagamentos).
$this->title / descriptionNome e texto exibidos ao cliente no checkout (vêm das configurações).
$this->has_fieldsSe true, o checkout renderiza payment_fields() (formulário próprio).
$this->supportsRecursos suportados (ex.: 'refunds', 'products', assinaturas).
init_form_fields()Define os campos de configuração do admin (Settings API).
init_settings()Carrega os valores salvos das configurações.
process_admin_options()(Herdado) Salva as configurações; pendurado no hook de salvamento.
payment_fields()Imprime o HTML do método no checkout (se has_fields).
validate_fields()Valida a entrada do cliente antes de process_payment().
process_payment( $order_id )Cobra no PSP e retorna array( 'result', 'redirect' ) ou falha.
process_refund( $order_id, $amount, $reason )Executa o estorno (se supports incluir 'refunds').
handle_webhook() (seu)Endpoint do callback do PSP; valida assinatura, confirma o pedido, idempotente.

Boas práticas e armadilhas comuns

Com o esqueleto acima você tem um gateway funcional e, mais importante, robusto: ele não derruba o site sem WooCommerce, confirma pagamentos pela fonte certa, resiste a webhooks repetidos e deixa rastro nos logs. A maior parte do esforço real de uma integração de pagamento não está no "caminho feliz" — está justamente nessas bordas. Se a sua loja precisa integrar um PSP específico, ou se um gateway existente está liberando pedidos não pagos ou perdendo confirmações, veja também erros de checkout e pagamento.

Precisa integrar um gateway de pagamento ao WooCommerce?

Desenvolvo gateways sob medida estendendo WC_Payment_Gateway, com webhook validado, idempotência e logs — do sandbox à produção. Me chame no WhatsApp e descreva o PSP.

WhatsApp: (43) 99932-9697

ou e-mail: [email protected]

Perguntas frequentes

O que é, na prática, um gateway de pagamento no WooCommerce?

É uma classe PHP que estende WC_Payment_Gateway e é registrada no WooCommerce pelo filtro woocommerce_payment_gateways. Essa classe descreve o método de pagamento (título, descrição, campos de configuração no admin) e implementa process_payment(), onde acontece a integração com o provedor (PSP). O WooCommerce cuida do resto: exibir a opção no checkout, salvar as configurações e gerenciar o ciclo do pedido.

Por que carregar a classe em plugins_loaded e checar class_exists?

Porque a sua classe estende WC_Payment_Gateway, que só existe depois que o WooCommerce carregou. Se você definir a classe cedo demais (ou com o WooCommerce desativado), o PHP dá fatal error por classe-pai inexistente. Por isso, defina/registre a classe num hook como plugins_loaded e proteja com if ( class_exists( 'WC_Payment_Gateway' ) ). Assim, sem WooCommerce, o seu plugin simplesmente não faz nada — em vez de derrubar o site.

Posso confiar no redirect de volta para confirmar o pagamento?

Não como única fonte de verdade. O redirect pode falhar: o cliente fecha o navegador, perde a conexão ou o PSP não redireciona. A confirmação confiável vem de um callback assíncrono (webhook/IPN) que o provedor envia direto ao seu servidor. Trate o redirect como "provavelmente pago, mostre a página de obrigado" e deixe o webhook ser quem realmente muda o status do pedido — sempre validando a assinatura e com idempotência.

Como evitar processar o mesmo pagamento duas vezes?

Com idempotência. O PSP pode reenviar o mesmo webhook várias vezes (retentativas, instabilidade). Antes de marcar o pedido como pago, cheque o status atual ($order->has_status()) e/ou uma meta de controle que você grava no pedido (ex.: o ID da transação já processada). Se já foi processado, responda 200 ao PSP e não faça nada — repetir payment_complete() ou reduzir estoque de novo causa erros e divergências de inventário.

Preciso de HTTPS/SSL para o gateway funcionar?

Sim, na prática é obrigatório. Você trafega dados sensíveis (de pagamento e do webhook) e a maioria dos PSPs exige HTTPS para receber callbacks e para conformidade (PCI-DSS). O próprio WooCommerce avisa quando o checkout não está sob SSL. Em produção, nunca opere um gateway sem certificado válido; em desenvolvimento, use o modo de teste (sandbox) do provedor.

Onde vejo o que o meu gateway está fazendo quando algo dá errado?

Nos logs do WooCommerce. Use wc_get_logger() e registre eventos com um source próprio (ex.: o id do gateway). Os logs ficam em WooCommerce › Status › Logs. Nunca registre dados sensíveis completos (números de cartão, chaves), mas registre IDs de transação, respostas de erro do PSP e o caminho que o código tomou — é o que vai te salvar ao depurar um pagamento que não confirmou. Veja também erros de checkout e pagamento.

Referências oficiais

  1. WooCommerce — Payment Gateway API
  2. WooCommerce — Developer Documentation
  3. WooCommerce — Documentation
Tem alguma dúvida? Ficarei feliz em ajudar. Clique Aqui