WordPress
Valida documentos en WordPress con un plugin basado en constaia/constaia-php, con shortcode, ruta REST protegida con nonce y perfiles en el servidor.
El repositorio del SDK de PHP incluye un plugin de ejemplo, examples/wordpress/constaia-upload.php, que añade un
shortcode con el widget y una ruta REST que llama a Constaia desde el servidor. Esta guía lo
instala, explica cómo funciona y cómo ampliarlo con tus propios perfiles y un webhook.
Es un plugin de ejemplo para copiar y adaptar, no un plugin publicado en el directorio de WordPress.org. Lo instalas
tú en wp-content/plugins y lo mantienes como parte de tu sitio.
Requisitos
- WordPress con PHP ≥ 8.1 (
ext-curlyext-json) y acceso a Composer en el servidor o en tu máquina de despliegue. - Una clave de test
ck_test_…del panel. En modo test no se consumen créditos y el resultado depende del nombre del fichero. - Usuarios con sesión iniciada: la ruta solo admite usuarios autenticados.
Instalación
Crea la carpeta del plugin e instala el SDK
mkdir -p wp-content/plugins/constaia-upload
cd wp-content/plugins/constaia-upload
composer require constaia/constaia-phpCopia el plugin
Guarda el código de la sección siguiente como wp-content/plugins/constaia-upload/constaia-upload.php.
Añade la clave a wp-config.php
define('CONSTAIA_API_KEY', 'ck_test_...');
define('CONSTAIA_WEBHOOK_SECRET', 'whsec_...'); // solo si añades el webhookPonlo antes de la línea /* That's all, stop editing! */. La clave nunca se imprime en la página: solo la usa el
servidor. Si prefieres variables de entorno, usa define('CONSTAIA_API_KEY', getenv('CONSTAIA_API_KEY'));.
Activa el plugin
En Plugins activa "Constaia Upload" (o wp plugin activate constaia-upload con WP-CLI) y añade el shortcode a
una página:
[constaia_upload profile="dni"]El plugin
Este es el plugin de ejemplo. El script del widget se carga con la build IIFE
(dist/cdn/constaia-widget.iife.min.js), que es la que funciona con wp_enqueue_script (un <script> clásico).
<?php
/**
* Plugin Name: Constaia Upload
* Description: Document validation with Constaia: [constaia_upload profile="dni"] shortcode + REST route constaia/v1/analyze.
* Version: 0.1.0
* Requires PHP: 8.1
* License: MIT
*/
declare(strict_types=1);
if (!defined('ABSPATH')) {
exit;
}
require_once __DIR__ . '/vendor/autoload.php';
/**
* What each profile validates. Decided here, on the server: the browser only picks a profile name.
* Extend it with add_filter('constaia_profiles', …).
*/
function constaia_profiles(): array
{
return apply_filters('constaia_profiles', [
'dni' => [
'expect' => ['es_dni', 'es_nie', 'passport'],
'checks' => ['not_expired' => true],
],
'medical' => [
'expect' => 'medical_certificate_sport',
'checks' => ['max_age_days' => 365, 'require_signature' => true],
],
]);
}
function constaia_client(): Constaia\Client
{
static $client = null;
$key = defined('CONSTAIA_API_KEY') ? CONSTAIA_API_KEY : null;
return $client ??= new Constaia\Client($key, ['timeout' => 60]);
}
add_action('rest_api_init', static function (): void {
register_rest_route('constaia/v1', '/analyze', [
'methods' => 'POST',
'callback' => 'constaia_rest_analyze',
// Standard REST cookie auth: WordPress validates the X-WP-Nonce header (wp_rest) and, without
// a valid nonce, treats the request as anonymous, so is_user_logged_in() is false.
'permission_callback' => static function (): bool {
return is_user_logged_in() && current_user_can('read');
},
'args' => [
'profile' => ['type' => 'string', 'required' => true, 'enum' => array_keys(constaia_profiles())],
],
]);
});
function constaia_rest_analyze(WP_REST_Request $request)
{
$profile = constaia_profiles()[$request->get_param('profile')] ?? null;
$file = $request->get_file_params()['file'] ?? null;
if ($profile === null || !is_array($file) || ($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
return constaia_rest_error('invalid_request', __('No document received.', 'constaia'), 400);
}
if (!is_uploaded_file($file['tmp_name']) || $file['size'] > 20 * 1024 * 1024) {
return constaia_rest_error('file_too_large', __('The file is too large (max. 20 MB).', 'constaia'), 400);
}
// Only the language is taken from the widget; expect/checks come from the server-side profile.
$clientOptions = json_decode((string) $request->get_param('options'), true);
$language = is_array($clientOptions) && in_array($clientOptions['language'] ?? '', ['es', 'en', 'pt', 'fr'], true)
? $clientOptions['language']
: 'es';
try {
$analysis = constaia_client()->analyze($file['tmp_name'], $profile + [
'filename' => sanitize_file_name($file['name']),
'storage' => 'none',
'language' => $language,
'metadata' => ['wp_user_id' => (string) get_current_user_id()],
]);
} catch (Constaia\Exception\ConstaiaException $e) {
error_log('[constaia] ' . $e->getMessage() . ' request_id=' . $e->requestId);
return constaia_rest_error('analysis_failed', __('The document could not be analysed. Please try again.', 'constaia'), 502);
}
do_action('constaia_analysis', $analysis, get_current_user_id());
return rest_ensure_response([
'id' => $analysis->id,
'object' => 'analysis',
'status' => $analysis->status,
'document' => $analysis->document?->toArray(),
'verdict' => $analysis->verdict?->toArray(),
'warnings' => $analysis->warnings ?? [],
]);
}
function constaia_rest_error(string $code, string $message, int $status): WP_REST_Response
{
return new WP_REST_Response(['error' => ['code' => $code, 'message' => $message]], $status);
}
add_shortcode('constaia_upload', static function ($atts): string {
$atts = shortcode_atts(['profile' => 'dni'], $atts, 'constaia_upload');
if (!is_user_logged_in()) {
return '<p>' . esc_html__('Log in to upload your document.', 'constaia') . '</p>';
}
if (!isset(constaia_profiles()[$atts['profile']])) {
return '';
}
wp_enqueue_script(
'constaia-widget',
'https://cdn.jsdelivr.net/npm/@constaia/widget@0.1/dist/cdn/constaia-widget.iife.min.js',
[],
'0.1.0',
true
);
$endpoint = add_query_arg(
['profile' => $atts['profile']],
rest_url('constaia/v1/analyze')
);
$headers = wp_json_encode(['X-WP-Nonce' => wp_create_nonce('wp_rest')]);
return sprintf(
'<constaia-upload endpoint="%s" headers="%s" lang="%s"></constaia-upload>',
esc_url($endpoint),
esc_attr((string) $headers),
esc_attr(substr(get_locale(), 0, 2))
);
});Cómo funciona
- La clave: se lee de la constante
CONSTAIA_API_KEYdewp-config.phpy solo la usaconstaia_client()en el servidor. El navegador nunca la ve; el widget rechaza cualquier atributo que parezca una clave. - El shortcode
[constaia_upload profile="dni"]solo se pinta para usuarios con sesión. Renderiza<constaia-upload>apuntando a/wp-json/constaia/v1/analyze?profile=dniy le pasa el noncewp_resten el atributoheaders, que el widget envía como cabeceraX-WP-Nonce. - La ruta REST
POST /wp-json/constaia/v1/analyzeusa la autenticación por cookie estándar de WordPress: sin un nonce válido la petición es anónima ypermission_callbackla rechaza. El parámetroprofilesolo admite los perfiles definidos. - Los perfiles deciden
expectychecksen el servidor. Del widget solo se tomalanguage; cualquier otra opción que envíe el navegador se ignora. - La respuesta devuelve solo
id,status,document,verdictywarnings: lo que el widget necesita para pintar el resultado. Los campos extraídos no salen del servidor. - La acción
constaia_analysisse dispara con el análisis y el id del usuario para que guardes el resultado.
Añadir perfiles con constaia_profiles
Amplía o cambia los perfiles desde tu tema o desde otro plugin con el filtro constaia_profiles. Como el filtro se
evalúa en cada petición, puedes usar datos del usuario actual, por ejemplo para comprobar el titular.
<?php
add_filter('constaia_profiles', static function (array $profiles): array {
$user = wp_get_current_user();
$profiles['dni'] = [
'expect' => ['es_dni', 'es_nie', 'passport'],
'checks' => [
'not_expired' => true,
'min_age_years' => 18,
'holder' => ['full_name' => $user->display_name],
],
];
$profiles['lopivi'] = [
'expect' => 'es_sexual_offences_certificate',
'checks' => [
'max_age_days' => 90,
'holder' => ['full_name' => $user->display_name],
],
];
$profiles['receipt'] = [
'expect' => 'payment_receipt',
'checks' => [
'expected_amount' => 45,
'expected_iban' => 'ES7921000813610123456789',
'expected_reference' => 'INSCRIPCION ' . $user->ID,
],
];
return $profiles;
});Uso: [constaia_upload profile="lopivi"] o [constaia_upload profile="receipt"]. Más sobre estos casos en
Certificado de delitos sexuales y
Justificantes de pago.
El certificado de delitos de naturaleza sexual incluye un código seguro de verificación (CSV). Constaia comprueba el
formato del código (csv_format), pero no consulta el servicio de verificación del Ministerio: si lo necesitas,
verifica el CSV en la sede oficial del emisor.
Guardar el resultado con constaia_analysis
<?php
add_action('constaia_analysis', static function (Constaia\Analysis $analysis, int $userId): void {
update_user_meta($userId, 'constaia_analysis_id', $analysis->id);
update_user_meta($userId, 'constaia_verdict', $analysis->verdictStatus() ?? $analysis->status);
if ($analysis->needsReview()) {
wp_mail(get_option('admin_email'), 'Documento pendiente de revisión', "Análisis {$analysis->id} del usuario {$userId}.");
}
}, 10, 2);verdictStatus() devuelve valid, invalid o review; si el análisis tarda más de 30 s la API responde 202, el
estado es queued o processing y el resultado llega por webhook. Qué hacer con cada estado en
Revisión humana.
Añadir un webhook
El plugin de ejemplo no recibe webhooks. Si usas 'async' => true en algún perfil, lotes o documentos largos, añade
una ruta REST pública que verifique la firma con el cuerpo crudo ($request->get_body()). Este código es tuyo, no
forma parte del plugin:
<?php
use Constaia\Exception\SignatureVerificationException;
use Constaia\Webhook;
add_action('rest_api_init', static function (): void {
register_rest_route('constaia/v1', '/webhook', [
'methods' => 'POST',
'permission_callback' => '__return_true', // la firma es la autenticación
'callback' => static function (WP_REST_Request $request) {
try {
$event = Webhook::verify($request->get_body(), $request->get_headers(), CONSTAIA_WEBHOOK_SECRET);
} catch (SignatureVerificationException $e) {
return new WP_REST_Response(['error' => 'invalid signature'], 400);
}
$webhookId = (string) $request->get_header('webhook-id');
if (get_transient('constaia_wh_' . md5($webhookId))) {
return new WP_REST_Response(null, 204);
}
set_transient('constaia_wh_' . md5($webhookId), 1, 4 * DAY_IN_SECONDS);
if (in_array($event['type'], ['analysis.completed', 'analysis.review_required', 'analysis.failed'], true)) {
$analysis = $event['data'];
$userId = (int) ($analysis['metadata']['wp_user_id'] ?? 0);
if ($userId > 0) {
update_user_meta($userId, 'constaia_verdict', $analysis['verdict']['status'] ?? $analysis['status']);
}
}
return new WP_REST_Response(null, 204);
},
]);
});Webhook::verify() acepta el formato de get_headers() de WordPress (claves en minúscula con guiones bajos y valores
en array). Crea el endpoint en el panel con la URL https://tu-sitio.com/wp-json/constaia/v1/webhook y guarda el
secret en CONSTAIA_WEBHOOK_SECRET. Formato y reintentos en Webhooks.
vendor/autoload.php ya lo carga el plugin; si pones este código en mu-plugins, que se carga antes, las clases del
SDK están disponibles cuando se ejecuta rest_api_init.
Errores
El plugin captura Constaia\Exception\ConstaiaException (la base de todas las excepciones del SDK), registra el
mensaje y el requestId con error_log() y devuelve 502 con un mensaje genérico. Si quieres mensajes distintos
según el caso, captura antes las excepciones concretas:
| Excepción | Cuándo | Mensaje sugerido |
|---|---|---|
InvalidRequestException | 400, 409, 413, 415, 422 (errorCode: file_too_large, unsupported_file_type, unreadable_image…) | Pide otro fichero |
InsufficientCreditsException | 402 | Avisa al administrador; al usuario, "inténtalo más tarde" |
RateLimitException | 429 tras los reintentos del SDK | "Inténtalo en unos minutos" |
AuthenticationException | 401 | Revisa CONSTAIA_API_KEY en wp-config.php |
ApiException, ConnectionException | 5xx, red | "Inténtalo más tarde" |
Códigos completos en Errores.
Probar en modo test
Con define('CONSTAIA_API_KEY', 'ck_test_...') en un entorno de pruebas, inicia sesión, abre la página con el
shortcode y sube ficheros reales (JPEG, PNG o PDF) con estos nombres. La tabla corresponde a los perfiles por
defecto del plugin; si añades la comprobación holder del filtro, el nombre del usuario debe coincidir con el del
documento de prueba (María García López en dni_valid.jpg).
| Fichero | Perfil | Resultado |
|---|---|---|
dni_valid.jpg | dni | valid: "Vigente hasta el 12/03/2031." |
dni_expired.jpg | dni | invalid: "Caducado el 15/06/2020." |
blurry.jpg | dni | review: aviso low_quality |
medical_certificate.pdf | medical | valid: firmado, apto |
El widget muestra el veredicto y los motivos. Todos los ficheros en Modo test.
Checklist de producción
php.inio panel del hosting:upload_max_filesize = 20M,post_max_size = 21M,max_execution_time = 90.- nginx
client_max_body_size 21M;yfastcgi_read_timeout 90s;(o el equivalente de tu hosting); timeouts ≥ 60 s en proxy y CDN delante de/wp-json/. - Solo usuarios con sesión pueden subir (lo hace el plugin) y conviene limitar la frecuencia en
/wp-json/constaia/v1/analyze(por ejemplolimit_reqen nginx o tu WAF): cada análisis live consume créditos. CONSTAIA_API_KEYconck_live_solo en producción; nunca en la base de datos de opciones ni en el tema.- Perfiles definidos en el servidor; no añadas atributos del shortcode que pasen
expectochecksdesde el contenido. - Webhook con firma verificada y deduplicado por
webhook-idsi usas procesamiento asíncrono. - Revisa Almacenamiento y privacidad; el plugin usa
storage: none.
Siguientes pasos
Symfony
Valida documentos en Symfony 6.4 y 7 con constaia/constaia-php, con el cliente como servicio, UploadedFile, Messenger para lo asíncrono y un webhook firmado.
Drupal
Valida documentos en Drupal 10 y 11 con un módulo propio, el cliente de constaia/constaia-php como servicio, un formulario Form API y un webhook firmado.