Recursos para desenvolvedores
O plugin é extensível de ponta a ponta só com PHP. Ações, categorias, gatilhos, integrações, condições, variáveis de texto, abas de configuração, rotas REST e canais de entrega são todos registráveis por filtros do WordPress — sem editar o core e sem escrever JavaScript.
Há dois caminhos equivalentes:
- Helpers (recomendado) — funções globais
joinotify_register_*(), carregadas com o plugin. - Filtros — os
apply_filters()por baixo dos helpers, quando você precisa de controle total.
Registre suas extensões em um gancho normal, como plugins_loaded ou init.
Vale o mesmo aviso de sempre para código em produção: tenha backup do site antes de instalar código PHP personalizado. O UpdraftPlus resolve isso gratuitamente.
Helpers de tempo de execução
Chamáveis de qualquer lugar — dentro de um handler de ação, do seu próprio gancho, de um callback REST.
| Helper | O que faz |
|---|---|
joinotify_send_whatsapp_message_text( $sender, $receiver, $message, $delay ) | Envia uma mensagem de texto; retorna o código HTTP |
joinotify_send_whatsapp_message_media( $sender, $receiver, $media_type, $media, $caption, $delay ) | Envia uma mensagem de mídia; retorna o código HTTP |
joinotify_dispatch_notification( $args ) | Envia por qualquer canal registrado; retorna um Channel_Result |
joinotify_replace_placeholders( $message, $payload, $mode ) | Resolve as variáveis {{ … }} contra um payload |
joinotify_prepare_message( $message, $payload ) | Resolve variáveis e tokens de IA |
joinotify_convert_html_to_whatsapp( $message ) | Converte HTML de texto rico para a formatação do WhatsApp |
joinotify_prepare_receiver( $receiver, $payload ) | Resolve variáveis e normaliza o telefone |
joinotify_format_phone( $phone ) | Valida e formata um telefone |
joinotify_get_placeholders( $integration, $trigger, $context ) | Lista as variáveis disponíveis |
joinotify_get_senders() | Lista os remetentes registrados |
joinotify_get_first_sender() | Primeiro remetente conectado |
joinotify_is_valid_sender( $sender ) | Verifica se o remetente é permitido |
joinotify_get_setting( $key ) | Lê uma configuração do plugin |
joinotify_get_message_history( $args ) | Consulta o histórico de mensagens |
joinotify_get_workflows( $args ) | Lista os fluxos |
joinotify_get_workflow_content( $post_id ) | Conteúdo (árvore de nós) de um fluxo |
joinotify_get_workflow_context( $post_id ) | Contexto do fluxo, por exemplo woocommerce |
joinotify_find_workflow_item( $content, $item_id ) | Encontra um nó dentro do fluxo |
joinotify_workflow_has_content( $post_id, $type ) | Verifica se o fluxo tem nós de um tipo |
joinotify_proxy_api_text_message_text_endpoint() | URL do endpoint de texto da Proxy API |
joinotify_proxy_api_media_message_text_endpoint() | URL do endpoint de mídia da Proxy API |
joinotify_get_proxy_api_key() | Chave configurada da Proxy API |
// Resolve as variáveis contra o payload e envia pelo primeiro remetente conectado.
$texto = joinotify_replace_placeholders( '{{ wc_billing_first_name }}, seu pedido saiu!', $payload );
$sender = joinotify_get_first_sender();
if ( $sender && joinotify_is_valid_sender( $sender ) ) {
joinotify_send_whatsapp_message_text( $sender, joinotify_format_phone( '11999998888' ), $texto );
}
Registrando extensões
| Helper | Filtro correspondente |
|---|---|
joinotify_register_action_category( $categoria ) | Joinotify/Builder/Action_Categories |
joinotify_register_action( $definicao ) | Joinotify/Builder/Actions |
joinotify_register_action_handler( $slug, $cb ) | Joinotify/Workflow_Processor/Handle_Actions |
joinotify_register_action_description( $slug, $cb ) | Joinotify/Builder/Action_Description |
joinotify_register_integration( $integracao ) | Joinotify/Settings/Tabs/Integrations |
joinotify_register_trigger( $contexto, $gatilho ) | Joinotify/Builder/Get_All_Triggers |
joinotify_dispatch_trigger( $hook, $integracao, $payload ) | disparo em tempo de execução |
joinotify_register_conditions( $gatilho, $conds ) | Joinotify/Validations/Get_Action_Conditions |
joinotify_register_condition_operators( $tipo, $ops ) | Joinotify/Conditions/Check_Condition_Type |
joinotify_register_condition_value( $tipo, $cb ) | Joinotify/Conditions/Get_Compare_Value |
joinotify_register_placeholders( $integracao, $vars ) | Joinotify/Builder/Placeholders_List |
joinotify_register_dynamic_placeholder( $regex, $cb ) | Joinotify/Builder/Resolve_Dynamic_Token |
joinotify_register_settings_tab( $aba ) | Joinotify/Admin/Settings/Section_Tabs |
joinotify_register_settings_section( $secao ) | Joinotify/Admin/Settings/Schema |
joinotify_register_rest_route( $rota ) | Joinotify/Rest/Routes |
joinotify_register_notification_channel( $id, $classe ) | Joinotify/Notifications/Channels |
Uma ação personalizada
Uma ação é um passo que o usuário arrasta para o canvas. Com o settings_schema declarado, o
formulário de configuração do nó é renderizado genericamente — nada de frontend.
joinotify_register_action( array(
'action' => 'meu_app_enviar_sms',
'title' => __( 'Meu App: enviar SMS', 'meu-textdomain' ),
'description' => __( 'Envia um SMS pelo Meu App.', 'meu-textdomain' ),
'category' => 'meu_app',
'category_label' => __( 'Meu App', 'meu-textdomain' ), // cria a aba automaticamente
'has_settings' => true,
'is_expansible' => true, // permite encadear ações depois desta
'context' => array(), // vazio = disponível em todos os gatilhos
'default_data' => array(
'action' => 'meu_app_enviar_sms',
'title' => __( 'Meu App: enviar SMS', 'meu-textdomain' ),
'to' => '{{ wc_billing_phone }}',
'message' => '',
),
'settings_schema' => array(
array( 'key' => 'to', 'label' => __( 'Destinatário', 'meu-textdomain' ), 'component' => 'input', 'required' => true ),
array( 'key' => 'message', 'label' => __( 'Mensagem', 'meu-textdomain' ), 'component' => 'textarea', 'required' => true, 'rows' => 4 ),
),
'handler' => function( $action_data, $action, $post_id, $event_data ) {
$to = joinotify_replace_placeholders( $action_data['to'] ?? '', $event_data );
$msg = joinotify_replace_placeholders( $action_data['message'] ?? '', $event_data );
// ... chame aqui o seu gateway de SMS ...
return true; // o handler deve retornar bool
},
) );
// A descrição que aparece sob o título do nó no canvas.
joinotify_register_action_description( 'meu_app_enviar_sms', function( $data, $workflow_action ) {
return esc_html( sprintf( __( 'SMS para %s', 'meu-textdomain' ), $data['to'] ?? '' ) );
} );
Componentes aceitos em settings_schema[].component: input, textarea, number, select
(com options), date, time, code, switch, attachments, além de group e repeater
aninhados. A chave condition controla visibilidade condicional, com os operadores eq, neq,
in, not_in, truthy e falsy.
Um gatilho e sua integração
Um gatilho só aparece no construtor quando a integração correspondente está registrada e
ativada (a opção em setting_key precisa estar em yes).
joinotify_register_integration( array(
'slug' => 'meu_app',
'title' => __( 'Meu App', 'meu-textdomain' ),
'description' => __( 'Automatize mensagens a partir de eventos do Meu App.', 'meu-textdomain' ),
'setting_key' => 'enable_meu_app_integration',
) );
joinotify_register_trigger( 'meu_app', array(
'data_trigger' => 'meu_app_pedido_pago',
'title' => __( 'Meu App: pedido pago', 'meu-textdomain' ),
'description' => __( 'Dispara quando um pedido do Meu App é pago.', 'meu-textdomain' ),
) );
// Dispare a partir do seu próprio gancho.
add_action( 'meu_app_pedido_pago', function( $order_id ) {
joinotify_dispatch_trigger( 'meu_app_pedido_pago', 'meu_app', array( 'order_id' => $order_id ) );
} );
O $payload passado no disparo fica disponível para variáveis e condições no momento do envio.
Novas variáveis de texto
joinotify_register_placeholders( 'woocommerce', array(
'{{ order_total }}' => array(
'triggers' => array(), // vazio = todos os gatilhos desta integração
'description' => esc_html__( 'Valor total do pedido', 'meu-textdomain' ),
'replacement' => array(
'sandbox' => 'R$ 99,99',
'production' => function( $payload ) {
$order = isset( $payload['order_id'] ) ? wc_get_order( $payload['order_id'] ) : null;
return $order ? wc_price( $order->get_total() ) : '';
},
),
),
) );
sandboxé o valor mostrado na pré-visualização do construtor;productioné o usado no envio, e pode ser umcallableresolvido na hora.triggerslista osdata_triggerem que a variável vale. Isso é aplicado também no envio desde a versão 2.1.0: um slug errado faz a variável simplesmente não resolver.
Para variáveis com argumento — {{ meu_app_campo=[total] }} — registre um resolvedor com PCRE:
joinotify_register_dynamic_placeholder( '/\{\{\s*meu_app_campo=\[(.+?)\]\s*\}\}/', function( $matches, $payload ) {
return $payload['fields'][ $matches[1] ] ?? null; // null mantém o token original
} );
Os resolvedores registrados rodam antes dos nativos, então dá para sobrescrever
{{ field_id=[…] }}, {{ wc_checkout_field=[…] }}, {{ wc_download_link=[…] }} e
{{ user_meta[…] }}.
Condições
joinotify_register_conditions( 'meu_app_pedido_pago', array(
'meu_app_plano' => array(
'title' => __( 'Plano da assinatura', 'meu-textdomain' ),
'description' => __( 'Verifica o plano do cliente.', 'meu-textdomain' ),
),
) );
joinotify_register_condition_operators( 'meu_app_plano', array( 'is', 'is_not' ) );
joinotify_register_condition_value( 'meu_app_plano', function( $value_map, $type, $payload ) {
return get_post_meta( $payload['order_id'] ?? 0, '_meu_app_plano', true );
} );
Uma rota REST
joinotify_register_rest_route( array(
'route' => '/admin/meu-app/ping',
'methods' => 'GET',
'callback' => function ( WP_REST_Request $request ) {
return rest_ensure_response( array( 'status' => 'success', 'pong' => true ) );
},
// 'permission' => fn() => current_user_can( 'manage_options' ), // padrão
) );
// → GET /wp-json/joinotify/v1/admin/meu-app/ping
Um canal de entrega
Um canal implementa Channel_Interface e passa a receber as mensagens dos fluxos. É assim que o
WhatsApp Cloud, o Telegram e o e-mail convivem no mesmo plugin.
use MeuMouse\Joinotify\Notifications\Channel_Interface;
use MeuMouse\Joinotify\Notifications\Notification_Message;
use MeuMouse\Joinotify\Notifications\Channel_Result;
class Meu_Canal implements Channel_Interface {
public function get_id() {
return 'meu_canal';
}
public function get_label() {
return __( 'Meu canal', 'meu-textdomain' );
}
public function is_configured() {
return '' !== get_option( 'meu_canal_token', '' );
}
public function get_capabilities() {
return array( 'text', 'media' );
}
public function supports( Notification_Message $message ) {
return '' !== trim( (string) $message->receiver )
&& in_array( $message->type, $this->get_capabilities(), true );
}
public function send( Notification_Message $message ) {
$ok = meu_canal_enviar( $message->receiver, $message->content );
return $ok
? Channel_Result::success( $this->get_id() )
: Channel_Result::failure( $this->get_id(), 'meu_canal_falhou', true );
}
}
joinotify_register_notification_channel( 'meu_canal', Meu_Canal::class );
Notification_Message carrega channel, type, sender, receiver, content, media_type,
media_url, caption, attachments, delay, um array context e um meta livre para campos
específicos do serviço. Leia o meta com $message->get_meta( $chave ).
Anexos modificam uma mensagem existente. Não os inclua em get_capabilities() — isso faria
supports() recusar mensagens comuns. Verifique $message->attachments em vez disso.
E nunca baixe um link de permissão de download do WooCommerce para reenviar os bytes: cada
download consumido é um download a menos para o cliente. Use o path já resolvido, ou mande o
link.
O mesmo desenho vale para os códigos de login sem senha, no filtro
Joinotify/Otp_Login/Channels.
Payload dos gatilhos nativos
Cada gatilho nativo entrega um conjunto de parâmetros no array $payload, disponível para
variáveis, condições e handlers de ação. Para ler o ID do pedido no gatilho
woocommerce_new_order, por exemplo:
$order_id = $payload['order_id'];
Três chaves existem em todos os gatilhos: type (sempre trigger), hook (o gancho que
disparou) e integration (o contexto — woocommerce, wordpress, elementor, wpforms…).
WooCommerce
| Gatilho | Quando dispara | Parâmetros adicionais |
|---|---|---|
Novo pedido (woocommerce_new_order) | Um pedido é criado, com qualquer status | order_id |
Novo pedido — processando (woocommerce_checkout_order_processed) | O pedido entra no status "processando" | order_id, status_transition |
Pedido concluído (woocommerce_order_status_completed) | O status muda para "concluído" | order_id, status_transition |
Status alterado (woocommerce_order_status_changed) | Qualquer mudança de status do pedido | order_id, old_status, new_status |
Reembolso total (woocommerce_order_fully_refunded) | O pedido é totalmente reembolsado | order_id, refund_id |
Reembolso parcial (woocommerce_order_partially_refunded) | O pedido é parcialmente reembolsado | order_id, refund_id |
Acesso a produto digital liberado (woocommerce_grant_product_download_permissions) | O WooCommerce libera o download ao cliente | order_id |
Arquivo digital baixado (woocommerce_download_product) | O cliente baixa um arquivo do pedido | order_id, product_id, download_id, user_id, user_email |
Para fluxos de entrega, use Acesso a produto digital liberado. Os links de download só existem a partir desse momento — em gatilhos anteriores as variáveis de download vêm vazias.
WooCommerce Subscriptions
| Gatilho | Parâmetros adicionais |
|---|---|
Assinatura criada (woocommerce_checkout_subscription_created) | subscription_id, order_id, recurring_cart |
Pagamento concluído (woocommerce_subscription_payment_complete) | subscription_id |
Pagamento falhou (woocommerce_subscription_payment_failed) | subscription_id |
Status alterado (woocommerce_subscription_status_active, …_cancelled, …_expired) | subscription_id, new_status |
WordPress
| Gatilho | Quando dispara | Parâmetros adicionais |
|---|---|---|
Novo registro (user_register) | Um usuário se registra | user_id, user_data |
Login (wp_login) | Um usuário faz login | user_id, user_data |
Solicitação de redefinição (retrieve_password) | O usuário pede o link de redefinição | user_id, reset_password_link |
Senha redefinida (password_reset) | O usuário conclui a redefinição | user_id |
Status do post alterado (transition_post_status) | Um post muda de status | post_id, post_type, post_status, old_post_status |
Elementor
| Gatilho | Parâmetros adicionais |
|---|---|
Formulário enviado (elementor_pro/forms/new_record) | id, fields, record, handler |
WPForms
| Gatilho | Parâmetros adicionais |
|---|---|
Formulário enviado (wpforms_process_complete) | id, fields, entry, form_data, entry_id |
Pagamento pelo PayPal (wpforms_paypal_standard_process_complete) | id, fields, entry, form_data, entry_id |
Filtros úteis
Novos formatos de arquivo
add_filter( 'Joinotify/Validations/Get_Mime_Types', function( $mime_types ) {
$mime_types['image'][] = 'image/svg+xml';
$mime_types['video'][] = 'video/webm';
$mime_types['document'][] = 'application/epub+zip';
$mime_types['audio'][] = 'audio/flac';
return $mime_types;
} );
Outros pontos
| Filtro | Para quê |
|---|---|
Joinotify/Builder/Action_Settings_Schema | Sobrescrever o schema de uma ação, inclusive nativa |
Joinotify/Builder/Action_Default_Data | Sobrescrever os dados padrão de uma ação |
Joinotify/Workflow_Processor/Delaying_Actions | Registrar uma ação que pausa o fluxo |
Joinotify/Workflow_Processor/Branching_Actions | Registrar uma ação que ramifica o fluxo |
Joinotify/Notifications/Default_Channel | Trocar o canal padrão de entrega |
Joinotify/Notifications/Message_Before_Send | Alterar a mensagem antes do envio |
Joinotify/Transport/Active | Forçar o transporte de WhatsApp resolvido |
Joinotify/Settings/Integrations/Categories | Registrar uma categoria própria na aba Aplicativos |
E a ação Joinotify/Notifications/Message_Sent, disparada depois de cada tentativa de envio.
Cuidados
- Registre cedo. Use
plugins_loadedouinit, antes do bootstrap do construtor. - O handler retorna
bool. Devolver outra coisa pode interromper o fluxo sem aviso. - Contexto do gatilho. O contexto só aparece quando o card da integração está registrado e ativado.
- Tudo é aditivo. Schemas, dados padrão e resolvedores nativos continuam valendo como fallback quando você não sobrescreve.
Referência completa
A referência exaustiva da API de extensão, sempre alinhada à versão mais recente, fica no repositório do plugin:
- DEVELOPERS.md — todos os pontos de extensão
- docs/integrations.md — o contrato declarativo dos cards de integração
- examples/ — uma extensão de exemplo, pronta para rodar