Overview
The Reviews API enables customers to leave ratings, comments, and photo reviews for products they have purchased. The API enforces purchase verification, preventing fake reviews, and supports up to 3 images per review.
Only users who have purchased a product (with order status: pagado, preparacion, enviado, or entregado) can leave reviews.
Get Product Reviews
GET /api/resenas.php?id_producto=42
Retrieves all reviews for a specific product, including user information, ratings, comments, and uploaded images. Also provides aggregate statistics and permission flags.
Query Parameters
The product ID to retrieve reviews for
Response Fields
Indicates if the request was successful
Array of review objects for the product
Optional review title (max 150 characters)
Review creation timestamp
Whether the review is from a verified purchaser (always 1 for this API)
Display name of the reviewer
URL to the reviewer’s profile photo
Array of uploaded review images (up to 3 per review)
Aggregate review statistics for the product
Average rating (1-5, rounded to 1 decimal place)
Count of reviews for each star rating (1-5)
Whether the current user can leave a review (true if they purchased but haven’t reviewed)
Whether the current user has already reviewed this product
Implementation Example
// Fetch product reviews
async function loadReviews(productId) {
const response = await fetch(`/api/resenas.php?id_producto=${productId}`);
const data = await response.json();
if (data.ok) {
displayReviews(data.resenas);
displayStats(data.stats);
// Show review form if user can review
if (data.puede_resenar && !data.ya_reseno) {
document.getElementById('review-form').style.display = 'block';
}
}
}
Create Review
POST /api/resenas.php
Content-Type: multipart/form-data
id_producto: 42
calificacion: 5
titulo: Excelente laptop
comentario: Muy buena calidad, llegó rápido y funciona perfectamente...
imagenes[]: [file1.jpg]
imagenes[]: [file2.jpg]
Creates a new review for a product. The user must be authenticated and must have purchased the product. Supports optional image uploads (up to 3 images, max 5MB each).
Authentication
Requires active user session. Returns 401 Unauthorized if not logged in.
// Session must contain authenticated user
if (!isset($_SESSION['usuario'])) {
http_response_code(401);
echo json_encode(['ok' => false, 'msg' => 'Debes iniciar sesión...']);
exit;
}
Request Parameters
Review comment (minimum 10 characters, maximum 2000 characters)
Optional review title (maximum 150 characters)
Optional array of image files (up to 3 images). Supported formats: JPEG, PNG, WebP, GIF. Maximum size: 5MB per image.
Response Fields
Indicates if the review was created successfully
The ID of the newly created review
Array of uploaded image URLs
Validation Rules
The API enforces the following validation rules:
// Rating must be 1-5
if ($calificacion < 1 || $calificacion > 5) {
return error('La calificación debe ser entre 1 y 5 estrellas.');
}
// Comment minimum length
if (empty($comentario) || mb_strlen($comentario) < 10) {
return error('El comentario debe tener al menos 10 caracteres.');
}
// Comment maximum length
if (mb_strlen($comentario) > 2000) {
return error('El comentario no puede exceder 2000 caracteres.');
}
// Title maximum length
if (mb_strlen($titulo) > 150) {
return error('El título no puede exceder 150 caracteres.');
}
Purchase Verification
Users can only review products they have purchased:
// Check if user has purchased the product
$chkCompra = $pdo->prepare("
SELECT p.id FROM pedidos p
INNER JOIN detalle_pedido dp ON dp.id_pedido = p.id
WHERE p.id_usuario = ? AND dp.id_producto = ?
AND p.estado IN ('pagado','preparacion','enviado','entregado')
LIMIT 1
");
The API prevents duplicate reviews - each user can only review a product once.
Image Upload
Images are processed with these constraints:
- Maximum files: 3 images per review
- Allowed types: JPEG, PNG, WebP, GIF
- Maximum size: 5MB per image
- Naming convention:
resena_{id}_{index}_{random}.{ext}
- Storage location:
uploads/resenas/
// Image validation
$allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
$maxSize = 5 * 1024 * 1024; // 5MB
for ($i = 0; $i < min(count($files['name']), 3); $i++) {
// Validate MIME type
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($files['tmp_name'][$i]);
if (!in_array($mime, $allowedTypes)) continue;
// Validate size
if ($files['size'][$i] > $maxSize) continue;
// Generate secure filename
$filename = 'resena_' . $resenaId . '_' . ($i + 1) . '_'
. bin2hex(random_bytes(4)) . '.' . $ext;
}
Error Responses
false when the review creation fails
Error message explaining why the review failed
Common Errors:
Debes iniciar sesión para dejar una reseña. (401)
Producto inválido.
La calificación debe ser entre 1 y 5 estrellas.
El comentario debe tener al menos 10 caracteres.
Ya dejaste una reseña para este producto.
Solo puedes dejar reseña de productos que hayas comprado.
Complete Implementation Example
<form id="review-form" enctype="multipart/form-data">
<input type="hidden" name="id_producto" value="42">
<label>Calificación:</label>
<select name="calificacion" required>
<option value="5">5 estrellas</option>
<option value="4">4 estrellas</option>
<option value="3">3 estrellas</option>
<option value="2">2 estrellas</option>
<option value="1">1 estrella</option>
</select>
<label>Título:</label>
<input type="text" name="titulo" maxlength="150" placeholder="Opcional">
<label>Comentario:</label>
<textarea name="comentario" required minlength="10" maxlength="2000"
placeholder="Cuéntanos tu experiencia (mínimo 10 caracteres)"></textarea>
<label>Imágenes (opcional, máximo 3):</label>
<input type="file" name="imagenes[]" accept="image/*" multiple>
<button type="submit">Publicar Reseña</button>
</form>
<script>
document.getElementById('review-form').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
try {
const response = await fetch('/api/resenas.php', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.ok) {
alert(result.msg);
location.reload(); // Refresh to show new review
} else {
alert('Error: ' + result.msg);
}
} catch (error) {
alert('Error al enviar la reseña. Intenta de nuevo.');
}
});
</script>
Get Pending Reviews
GET /api/pendientes_resena.php
Retrieves products that the current user has received (estado = 'entregado') but hasn’t reviewed yet. Useful for prompting customers to leave feedback.
Authentication
Returns empty array if user is not logged in (not an error condition).
Response Fields
Always true (returns empty array on error instead of failing)
Array of products awaiting review (maximum 5 most recent)
pendientes[].fecha_entrega
Date when the order was delivered
Implementation Example
// Display pending reviews widget
async function loadPendingReviews() {
const response = await fetch('/api/pendientes_resena.php');
const data = await response.json();
if (data.ok && data.pendientes.length > 0) {
const widget = document.getElementById('pending-reviews-widget');
widget.innerHTML = '<h3>Productos pendientes de reseñar</h3>';
data.pendientes.forEach(product => {
widget.innerHTML += `
<div class="pending-review-item">
<img src="${product.imagen}" alt="${product.nombre}">
<h4>${product.nombre}</h4>
<p>Entregado: ${product.fecha_entrega}</p>
<a href="/producto.php?id=${product.id}#reviews">
Dejar reseña
</a>
</div>
`;
});
widget.style.display = 'block';
}
}
Database Schema
resenas (Reviews)
CREATE TABLE resenas (
id INT AUTO_INCREMENT PRIMARY KEY,
id_producto INT NOT NULL,
id_usuario INT NOT NULL,
calificacion TINYINT NOT NULL DEFAULT 5,
titulo VARCHAR(150) NULL,
comentario TEXT NULL,
fecha DATETIME DEFAULT CURRENT_TIMESTAMP,
verificado TINYINT(1) DEFAULT 1,
FOREIGN KEY (id_producto) REFERENCES productos(id) ON DELETE CASCADE,
FOREIGN KEY (id_usuario) REFERENCES usuarios(id) ON DELETE CASCADE,
UNIQUE KEY uniq_resena_usuario_producto (id_usuario, id_producto),
INDEX idx_producto (id_producto),
INDEX idx_fecha (fecha)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
resenas_imagenes (Review Images)
CREATE TABLE resenas_imagenes (
id INT AUTO_INCREMENT PRIMARY KEY,
id_resena INT NOT NULL,
url_imagen VARCHAR(500) NOT NULL,
fecha DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (id_resena) REFERENCES resenas(id) ON DELETE CASCADE,
INDEX idx_resena (id_resena)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Both tables are automatically created by the API if they don’t exist. The schema uses utf8mb4 charset for full emoji support.