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:
- O registro: um
add_filter( 'woocommerce_payment_gateways', … )que adiciona o nome da sua classe ao array de gateways conhecidos. - 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. - 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 / propriedade | Responsabilidade |
|---|---|
$this->id | Identificador único do gateway; entra no nome de vários hooks. |
$this->method_title / method_description | Nome e descrição exibidos na tela de admin (lista de pagamentos). |
$this->title / description | Nome e texto exibidos ao cliente no checkout (vêm das configurações). |
$this->has_fields | Se true, o checkout renderiza payment_fields() (formulário próprio). |
$this->supports | Recursos 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
- Carregue no
plugins_loadedcomclass_exists(). EstenderWC_Payment_Gatewayantes de o WooCommerce existir é fatal error garantido. - Webhook é a fonte de verdade. Não libere pedido só pelo redirect. Em Pix/boleto, o redirect nem confirma pagamento — só o callback do PSP confirma.
- Idempotência sempre. Cheque
$order->has_status()e/ou uma meta antes de chamarpayment_complete(). PSPs reenviam webhooks. - Valide a assinatura do webhook. Use
hash_hmac()+hash_equals(). Um endpoint sem validação é uma porta para marcar pedidos como pagos sem pagamento. - HTTPS obrigatório. Sem SSL não há gateway sério: dados sensíveis, callbacks e conformidade PCI exigem.
- Modo teste x produção. Separe chaves e URLs por ambiente; nunca deixe o sandbox ativo em produção (nem o contrário).
- Logue com
wc_get_logger(). Comsourcepróprio, sem dados sensíveis. É o que permite depurar pagamento que não confirmou. - Use a CRUD API.
wc_get_order(),$order->get_total(),$order->payment_complete()— nuncaget_post()/get_post_meta()direto, por compatibilidade com o armazenamento moderno de pedidos. - Sanitize toda entrada. Tudo que vem de
$_POSTou do corpo do webhook passa porsanitize_text_field(),absint()etc. Veja segurança no código.
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.
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.