Recursos para desarrolladores
El plugin es extensible de principio a fin solo con PHP. Acciones, categorías, disparadores, integraciones, condiciones, variables de texto, pestañas de configuración, rutas REST y canales de entrega son todos registrables mediante filtros de WordPress — sin editar el núcleo y sin escribir JavaScript.
Hay dos caminos equivalentes:
- Helpers (recomendado) — funciones globales
joinotify_register_*(), cargadas con el plugin. - Filtros — los
apply_filters()que hay debajo, cuando necesitas control total.
Registra tus extensiones en un hook normal, como plugins_loaded o init.
Vale la advertencia de siempre para el código en producción: respalda el sitio antes de instalar código PHP personalizado. UpdraftPlus lo hace gratis.
Helpers en tiempo de ejecución
Se pueden llamar desde cualquier sitio — dentro de un handler de acción, de tu propio hook, de un callback REST.
| Helper | Qué hace |
|---|---|
joinotify_send_whatsapp_message_text( $sender, $receiver, $message, $delay ) | Envía un mensaje de texto; devuelve el código HTTP |
joinotify_send_whatsapp_message_media( $sender, $receiver, $media_type, $media, $caption, $delay ) | Envía un mensaje multimedia; devuelve el código HTTP |
joinotify_dispatch_notification( $args ) | Envía por cualquier canal registrado; devuelve un Channel_Result |
joinotify_replace_placeholders( $message, $payload, $mode ) | Resuelve las variables {{ … }} contra un payload |
joinotify_prepare_message( $message, $payload ) | Resuelve variables y tokens de IA |
joinotify_convert_html_to_whatsapp( $message ) | Convierte HTML de texto enriquecido al formato de WhatsApp |
joinotify_prepare_receiver( $receiver, $payload ) | Resuelve variables y normaliza el teléfono |
joinotify_format_phone( $phone ) | Valida y formatea un teléfono |
joinotify_get_placeholders( $integration, $trigger, $context ) | Lista las variables disponibles |
joinotify_get_senders() | Lista los remitentes registrados |
joinotify_get_first_sender() | Primer remitente conectado |
joinotify_is_valid_sender( $sender ) | Comprueba si el remitente está permitido |
joinotify_get_setting( $key ) | Lee una opción del plugin |
joinotify_get_message_history( $args ) | Consulta el historial de mensajes |
joinotify_get_workflows( $args ) | Lista los flujos |
joinotify_get_workflow_content( $post_id ) | Contenido (árbol de nodos) de un flujo |
joinotify_get_workflow_context( $post_id ) | Contexto del flujo, por ejemplo woocommerce |
joinotify_find_workflow_item( $content, $item_id ) | Encuentra un nodo dentro del flujo |
joinotify_workflow_has_content( $post_id, $type ) | Comprueba si el flujo tiene nodos de un tipo |
joinotify_proxy_api_text_message_text_endpoint() | URL del endpoint de texto de la Proxy API |
joinotify_proxy_api_media_message_text_endpoint() | URL del endpoint multimedia de la Proxy API |
joinotify_get_proxy_api_key() | Clave configurada de la Proxy API |
// Resuelve las variables contra el payload y envía por el primer remitente conectado.
$texto = joinotify_replace_placeholders( '{{ wc_billing_first_name }}, ¡tu pedido va en camino!', $payload );
$sender = joinotify_get_first_sender();
if ( $sender && joinotify_is_valid_sender( $sender ) ) {
joinotify_send_whatsapp_message_text( $sender, joinotify_format_phone( '11999998888' ), $texto );
}
Registrar extensiones
| Helper | Filtro correspondiente |
|---|---|
joinotify_register_action_category( $categoria ) | Joinotify/Builder/Action_Categories |
joinotify_register_action( $definicion ) | 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( $integracion ) | Joinotify/Settings/Tabs/Integrations |
joinotify_register_trigger( $contexto, $disparador ) | Joinotify/Builder/Get_All_Triggers |
joinotify_dispatch_trigger( $hook, $integracion, $payload ) | disparo en tiempo de ejecución |
joinotify_register_conditions( $disparador, $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( $integracion, $vars ) | Joinotify/Builder/Placeholders_List |
joinotify_register_dynamic_placeholder( $regex, $cb ) | Joinotify/Builder/Resolve_Dynamic_Token |
joinotify_register_settings_tab( $pestana ) | Joinotify/Admin/Settings/Section_Tabs |
joinotify_register_settings_section( $seccion ) | Joinotify/Admin/Settings/Schema |
joinotify_register_rest_route( $ruta ) | Joinotify/Rest/Routes |
joinotify_register_notification_channel( $id, $clase ) | Joinotify/Notifications/Channels |
Una acción personalizada
Una acción es un paso que el usuario arrastra al canvas. Con el settings_schema declarado, el
formulario de configuración del nodo se renderiza de forma genérica — sin tocar el frontend.
joinotify_register_action( array(
'action' => 'mi_app_enviar_sms',
'title' => __( 'Mi App: enviar SMS', 'mi-textdomain' ),
'description' => __( 'Envía un SMS por Mi App.', 'mi-textdomain' ),
'category' => 'mi_app',
'category_label' => __( 'Mi App', 'mi-textdomain' ), // crea la pestaña automáticamente
'has_settings' => true,
'is_expansible' => true, // permite encadenar acciones después de esta
'context' => array(), // vacío = disponible en todos los disparadores
'default_data' => array(
'action' => 'mi_app_enviar_sms',
'title' => __( 'Mi App: enviar SMS', 'mi-textdomain' ),
'to' => '{{ wc_billing_phone }}',
'message' => '',
),
'settings_schema' => array(
array( 'key' => 'to', 'label' => __( 'Destinatario', 'mi-textdomain' ), 'component' => 'input', 'required' => true ),
array( 'key' => 'message', 'label' => __( 'Mensaje', 'mi-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 );
// ... llama aquí a tu pasarela de SMS ...
return true; // el handler debe devolver bool
},
) );
// La descripción que aparece bajo el título del nodo en el canvas.
joinotify_register_action_description( 'mi_app_enviar_sms', function( $data, $workflow_action ) {
return esc_html( sprintf( __( 'SMS a %s', 'mi-textdomain' ), $data['to'] ?? '' ) );
} );
Componentes aceptados en settings_schema[].component: input, textarea, number, select
(con options), date, time, code, switch, attachments, además de group y repeater
anidados. La clave condition controla la visibilidad condicional, con los operadores eq,
neq, in, not_in, truthy y falsy.
Un disparador y su integración
Un disparador solo aparece en el constructor cuando la integración correspondiente está
registrada y activada (su opción en setting_key en yes).
joinotify_register_integration( array(
'slug' => 'mi_app',
'title' => __( 'Mi App', 'mi-textdomain' ),
'description' => __( 'Automatiza mensajes a partir de eventos de Mi App.', 'mi-textdomain' ),
'setting_key' => 'enable_mi_app_integration',
) );
joinotify_register_trigger( 'mi_app', array(
'data_trigger' => 'mi_app_pedido_pagado',
'title' => __( 'Mi App: pedido pagado', 'mi-textdomain' ),
'description' => __( 'Se dispara cuando se paga un pedido de Mi App.', 'mi-textdomain' ),
) );
// Dispáralo desde tu propio hook.
add_action( 'mi_app_pedido_pagado', function( $order_id ) {
joinotify_dispatch_trigger( 'mi_app_pedido_pagado', 'mi_app', array( 'order_id' => $order_id ) );
} );
El $payload que pasas en el disparo queda disponible para variables y condiciones en el momento
del envío.
Nuevas variables de texto
joinotify_register_placeholders( 'woocommerce', array(
'{{ order_total }}' => array(
'triggers' => array(), // vacío = todos los disparadores de esta integración
'description' => esc_html__( 'Total del pedido', 'mi-textdomain' ),
'replacement' => array(
'sandbox' => '99,99 €',
'production' => function( $payload ) {
$order = isset( $payload['order_id'] ) ? wc_get_order( $payload['order_id'] ) : null;
return $order ? wc_price( $order->get_total() ) : '';
},
),
),
) );
sandboxes el valor que se muestra en la vista previa del constructor;productiones el que se usa en el envío, y puede ser uncallableresuelto en el momento.triggerslista losdata_triggeren los que vale la variable. Esto se aplica también en el envío desde la versión 2.1.0: un slug equivocado hace que la variable simplemente no se resuelva.
Para variables con argumento — {{ mi_app_campo=[total] }} — registra un resolvedor con PCRE:
joinotify_register_dynamic_placeholder( '/\{\{\s*mi_app_campo=\[(.+?)\]\s*\}\}/', function( $matches, $payload ) {
return $payload['fields'][ $matches[1] ] ?? null; // null mantiene el token original
} );
Los resolvedores registrados se ejecutan antes que los nativos, así que puedes sobrescribir
{{ field_id=[…] }}, {{ wc_checkout_field=[…] }}, {{ wc_download_link=[…] }} y
{{ user_meta[…] }}.
Condiciones
joinotify_register_conditions( 'mi_app_pedido_pagado', array(
'mi_app_plan' => array(
'title' => __( 'Plan de la suscripción', 'mi-textdomain' ),
'description' => __( 'Comprueba el plan del cliente.', 'mi-textdomain' ),
),
) );
joinotify_register_condition_operators( 'mi_app_plan', array( 'is', 'is_not' ) );
joinotify_register_condition_value( 'mi_app_plan', function( $value_map, $type, $payload ) {
return get_post_meta( $payload['order_id'] ?? 0, '_mi_app_plan', true );
} );
Una ruta REST
joinotify_register_rest_route( array(
'route' => '/admin/mi-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' ), // predeterminado
) );
// → GET /wp-json/joinotify/v1/admin/mi-app/ping
Un canal de entrega
Un canal implementa Channel_Interface y pasa a recibir los mensajes de los flujos. Así es como
WhatsApp Cloud, Telegram y el correo conviven en el mismo plugin.
use MeuMouse\Joinotify\Notifications\Channel_Interface;
use MeuMouse\Joinotify\Notifications\Notification_Message;
use MeuMouse\Joinotify\Notifications\Channel_Result;
class Mi_Canal implements Channel_Interface {
public function get_id() {
return 'mi_canal';
}
public function get_label() {
return __( 'Mi canal', 'mi-textdomain' );
}
public function is_configured() {
return '' !== get_option( 'mi_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 = mi_canal_enviar( $message->receiver, $message->content );
return $ok
? Channel_Result::success( $this->get_id() )
: Channel_Result::failure( $this->get_id(), 'mi_canal_fallo', true );
}
}
joinotify_register_notification_channel( 'mi_canal', Mi_Canal::class );
Notification_Message lleva channel, type, sender, receiver, content, media_type,
media_url, caption, attachments, delay, un array context y un meta libre para campos
propios del servicio. Lee el meta con $message->get_meta( $clave ).
Los adjuntos modifican un mensaje existente. No los incluyas en get_capabilities() — eso haría
que supports() rechazara los mensajes normales. Comprueba $message->attachments en su lugar.
Y nunca descargues un enlace de permiso de descarga de WooCommerce para reenviar los bytes: cada
descarga consumida es una descarga menos para el cliente. Usa el path ya resuelto, o manda el
link.
El mismo diseño vale para los códigos de inicio de sesión sin contraseña, en el filtro
Joinotify/Otp_Login/Channels.
Payload de los disparadores nativos
Cada disparador nativo entrega un conjunto de parámetros en el array $payload, disponible
para variables, condiciones y handlers de acción. Para leer el ID del pedido en el disparador
woocommerce_new_order, por ejemplo:
$order_id = $payload['order_id'];
Tres claves existen en todos los disparadores: type (siempre trigger), hook (el gancho que
disparó) e integration (el contexto — woocommerce, wordpress, elementor, wpforms…).
WooCommerce
| Disparador | Cuándo se dispara | Parámetros adicionales |
|---|---|---|
Nuevo pedido (woocommerce_new_order) | Se crea un pedido, con cualquier estado | order_id |
Nuevo pedido — procesando (woocommerce_checkout_order_processed) | El pedido entra en estado "procesando" | order_id, status_transition |
Pedido completado (woocommerce_order_status_completed) | El estado pasa a "completado" | order_id, status_transition |
Estado cambiado (woocommerce_order_status_changed) | Cualquier cambio de estado del pedido | order_id, old_status, new_status |
Reembolso total (woocommerce_order_fully_refunded) | El pedido se reembolsa por completo | order_id, refund_id |
Reembolso parcial (woocommerce_order_partially_refunded) | El pedido se reembolsa parcialmente | order_id, refund_id |
Acceso digital concedido (woocommerce_grant_product_download_permissions) | WooCommerce concede la descarga al cliente | order_id |
Archivo digital descargado (woocommerce_download_product) | El cliente descarga un archivo del pedido | order_id, product_id, download_id, user_id, user_email |
Para flujos de entrega, usa Acceso digital concedido. Los enlaces de descarga solo existen a partir de ese momento — en disparadores anteriores las variables de descarga vienen vacías.
WooCommerce Subscriptions
| Disparador | Parámetros adicionales |
|---|---|
Suscripción creada (woocommerce_checkout_subscription_created) | subscription_id, order_id, recurring_cart |
Pago completado (woocommerce_subscription_payment_complete) | subscription_id |
Pago fallido (woocommerce_subscription_payment_failed) | subscription_id |
Estado cambiado (woocommerce_subscription_status_active, …_cancelled, …_expired) | subscription_id, new_status |
WordPress
| Disparador | Cuándo se dispara | Parámetros adicionales |
|---|---|---|
Nuevo registro (user_register) | Un usuario se registra | user_id, user_data |
Inicio de sesión (wp_login) | Un usuario inicia sesión | user_id, user_data |
Solicitud de restablecimiento (retrieve_password) | El usuario pide el enlace de restablecimiento | user_id, reset_password_link |
Contraseña restablecida (password_reset) | El usuario completa el restablecimiento | user_id |
Estado de entrada cambiado (transition_post_status) | Una entrada cambia de estado | post_id, post_type, post_status, old_post_status |
Elementor
| Disparador | Parámetros adicionales |
|---|---|
Formulario enviado (elementor_pro/forms/new_record) | id, fields, record, handler |
WPForms
| Disparador | Parámetros adicionales |
|---|---|
Formulario enviado (wpforms_process_complete) | id, fields, entry, form_data, entry_id |
Pago por PayPal (wpforms_paypal_standard_process_complete) | id, fields, entry, form_data, entry_id |
Filtros útiles
Nuevos formatos de archivo
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;
} );
Otros puntos
| Filtro | Para qué sirve |
|---|---|
Joinotify/Builder/Action_Settings_Schema | Sobrescribir el esquema de una acción, incluso nativa |
Joinotify/Builder/Action_Default_Data | Sobrescribir los datos predeterminados de una acción |
Joinotify/Workflow_Processor/Delaying_Actions | Registrar una acción que pausa el flujo |
Joinotify/Workflow_Processor/Branching_Actions | Registrar una acción que ramifica el flujo |
Joinotify/Notifications/Default_Channel | Cambiar el canal de entrega predeterminado |
Joinotify/Notifications/Message_Before_Send | Modificar el mensaje antes del envío |
Joinotify/Transport/Active | Forzar el transporte de WhatsApp resuelto |
Joinotify/Settings/Integrations/Categories | Registrar una categoría propia en la pestaña Aplicaciones |
Y la acción Joinotify/Notifications/Message_Sent, disparada tras cada intento de envío.
Precauciones
- Registra pronto. Usa
plugins_loadedoinit, antes del bootstrap del constructor. - El handler devuelve
bool. Devolver otra cosa puede interrumpir el flujo sin avisar. - Contexto del disparador. El contexto solo aparece cuando la tarjeta de la integración está registrada y activada.
- Todo es aditivo. Esquemas, datos predeterminados y resolvedores nativos siguen valiendo como respaldo cuando no los sobrescribes.
Referencia completa
La referencia exhaustiva de la API de extensión, siempre alineada con la última versión, está en el repositorio del plugin:
- DEVELOPERS.md — todos los puntos de extensión
- docs/integrations.md — el contrato declarativo de las tarjetas de integración
- examples/ — una extensión de ejemplo lista para ejecutar