Inicio rápido
Tres pasos de la clave al archivo terminado. Un sitio tarda de media alrededor de un minuto en generarse, por eso el pedido devuelve un ID de tarea y no el sitio: después consultas el estado y descargas el ZIP.
# 1. Pedir un sitio
curl -X POST https://exstru.com/api/v1/generate \
-H 'Authorization: Bearer sg_tu_clave' \
-H 'Content-Type: application/json' \
-d '{ "tier": "premium", "siteType": "local-business",
"niche": "odontología", "language": "es", "country": "ES" }'
# → { "jobId": "V1StGXR8Z5jd", "queue": { "position": 3, … } }
# 2. Consultar el estado cada 3–5 segundos
curl https://exstru.com/api/v1/status/V1StGXR8Z5jd \
-H 'Authorization: Bearer sg_tu_clave'
# 3. Descargar el archivo cuando status = completed
curl -o site.zip https://exstru.com/api/v1/download/V1StGXR8Z5jd \
-H 'Authorization: Bearer sg_tu_clave'Acceso y clave
La API se conecta al equipo. Una clave del tipo sg_… la genera el propietario en la sección «API» del bot; allí también se regenera y se revoca. Los créditos de los sitios se descuentan del saldo común del equipo.
Cómo enviar la clave
En cualquiera de los dos encabezados — son equivalentes. El segundo sirve si tu librería ya ocupa Authorization.
Authorization: Bearer sg_tu_clave X-Api-Key: sg_tu_clave
Planes en la solicitud
El plan se indica en el campo tier y determina cuántos créditos se descuentan y qué recibe el sitio. La suscripción a la API no limita el plan: con una misma clave puedes pedir cualquiera.
basicpremiumeliteLa comparación completa de funciones está en la página «Precios».
Pedir un sitio
Pone un sitio en cola y descuenta créditos según el plan. La respuesta llega al instante, con el ID de la tarea y el puesto en la cola.
Parámetros del cuerpo de la solicitud
Reglas de los campos
Los errores de formato se devuelven todos a la vez: 400 invalid_request, la lista está en data.issues. No se cobra ningún crédito. Los campos desconocidos se ignoran.
companyInfoobjectSi envías el objeto, name, email y phone son obligatorios. Sin el objeto, el generador inventará el nombre y los contactos. Todos los campos son cadenas.
brandColorstring- Formato —
#rrggbbo el formato corto#rgb, sin distinguir mayúsculas; los espacios de los extremos se eliminan.#1E6FD9y#1e6fd9son el mismo color,#16dse lee como#1166dd. - Cadena vacía o campo ausente: la paleta se elige según el nicho.
- Cualquier otro formato —
rgb(), nombres de colores, un código sin#— error400 invalid_request. - El color no se copia tal cual: cada sitio recibe un tono cercano propio, para que los sitios de un mismo equipo no se repitan entre sí.
- En Basic el campo se acepta, pero no se aplica — aviso
brand_color_not_in_tier.
baseUrlstring · hasta 253 caracteres- Dominio en caracteres latinos: letras, cifras y guiones, con al menos un punto. Los dominios en cirílico, en punycode:
xn--… - El esquema es opcional:
http://yhttps://se aceptan; los enlaces del sitio siempre usaránhttps. wwwse conserva: tú decides qué host es el canónico. Se admite una ruta para un sitio en una subcarpeta.- Barra final, parámetros
?…y el ancla#…se descartan. - No se admiten: espacios, puerto, dirección IP, usuario en la dirección (
user@) — error400 invalid_request. - Sin dominio, el sitio se genera sin
sitemap.xml,canonical,og:urlyhreflang: necesitan una dirección completa.
extraLanguagesstring[]- Códigos — igual que en
language: códigos ISO 639-1 de dos letras en minúscula de la lista de admitidos. Las variantes regionales (en-GB,pt-BR) no se aceptan. - El idioma principal va en la raíz del sitio y se marca como
x-default. Los idiomas adicionales van en subcarpetas/en/,/de/en el orden del array. - Los duplicados y el idioma principal dentro de
extraLanguagesse omiten y no cuentan para el límite. - Límite del plan — Premium 2, Elite 9 adicionales, Basic 0. Los idiomas sobrantes se descartan desde el final del array y el precio del pedido no cambia — aviso
language_limit. - Código no admitido en
languageoextraLanguages— error400 invalid_request. - Si el nicho no se puede hacer en uno de los idiomas (por ejemplo, VPN en ruso) —
422 niche_language_refused, y no se crea el pedido. Quita ese idioma y vuelve a intentarlo.
"tier": "premium",
"language": "es",
"extraLanguages": ["es", "en", "en", "de", "fr"]
→ idiomas del sitio: es (raíz), /en/, /de/
→ "warnings": [{
"code": "language_limit",
"field": "extraLanguages",
"message": "Premium: hasta 3 idiomas, fr no añadido"
}]countrystring · 2 letras- Código ISO 3166-1, sin distinguir mayúsculas:
mxyMXson el mismo país. - Compatibles:
US GB CA AU IE NZ DE AT CH ES MX CO CL FR BE RU KZ IT PT BR NL PL CZ RO SE DK NO. - El país no cambia el idioma del sitio; lo define
language. Un sitio en inglés para una empresa alemana es"language": "en", "country": "DE": precios en euros, teléfono+49, imprint en inglés. - Sin el campo o con un código fuera de la lista: error
400 invalid_requestcon el campocountry, no se descuentan créditos.
"language": "es", "country": "MX" → precios en pesos → teléfono +52 55 1234 5678 → <html lang="es-MX">
archiveFormatstring · html | phphtml(por defecto) — sitio estático: cualquier hosting, Netlify, GitHub Pages.php— las mismas páginas, más un gestor de solicitudes: los formularios las envían acompanyInfo.email. Requiere hosting con PHP 7.0 o superior en el que funcione el envío de correo (mail()).- Sin
companyInfo.emailel sitio recibe contactos de demostración y no hay correo para las solicitudes: los formularios no las aceptan hasta que escribas una dirección al principio del archivo PHP. - El precio no depende del formato. Cualquier otro valor es un error
400 invalid_request.
Ejemplo
POST /api/v1/generate
Authorization: Bearer sg_tu_clave
Content-Type: application/json
{
"tier": "premium",
"siteType": "local-business",
"niche": "odontología",
"language": "es",
"country": "ES",
"extraLanguages": ["en", "de"],
"baseUrl": "sonrisa-clinica.es",
"brandColor": "#1e6fd9",
"keywords": ["implantes", "ortodoncia"],
"companyInfo": {
"name": "Sonrisa",
"email": "hola@sonrisa.es",
"phone": "+34 600 000 000"
}
}{
"success": true,
"jobId": "V1StGXR8Z5jd",
"queue": {
"position": 3,
"etaSeconds": 90
},
"warnings": []
}Campos de la respuesta
Consultar el estado
Indica en qué etapa está el pedido. Consulta cada 3–5 segundos hasta que el estado sea final: completed, failed o cancelled.
{
"jobId": "V1StGXR8Z5jd",
"status": "processing",
"queuePosition": null,
"etaSeconds": null,
"progress": 55,
"step": "Textos listos, buscando fotos…",
"error": null,
"ready": false
}Estados
Cancelar un pedido
Funciona mientras el pedido espera en la cola (pending). Los créditos vuelven al saldo al instante. Si la generación ya empezó, no se puede cancelar — la respuesta es 409 not_waiting.
{ "jobId": "V1StGXR8Z5jd", "status": "cancelled", "refunded": true }Descargar el archivo
Devuelve application/zip en streaming cuando el estado es completed. El archivo se guarda 14 días y puedes descargarlo tantas veces como quieras. Después, la respuesta es 410 gone.
Errores
El código de error legible por máquina está en data.code. Básate en él: el texto del mensaje puede cambiar.
{
"statusCode": 422,
"message": "Este nicho no lo generamos: …",
"data": {
"code": "niche_refused",
"category": "gambling",
"reason": "juegos de azar",
"alternatives": [
{ "label": "club de juegos de mesa", "canonical": null }
]
}
}Una tarea ajena responde con el mismo 404 que un trabajo inexistente — así los ID de trabajo no se pueden adivinar probando uno tras otro.
Límites y cola
Con 429 llega la cabecera Retry-After con el número de segundos. Espera ese tiempo: repetir antes no evita el límite, lo prolonga. Los límites se cuentan por clave, no por dirección IP.