Quick start
Three steps from a key to a finished archive. A site takes about a minute to build on average, so an order returns a job ID, not a site: then you poll the status and download the ZIP.
# 1. Order a site
curl -X POST https://exstru.com/api/v1/generate \
-H 'Authorization: Bearer sg_your_key' \
-H 'Content-Type: application/json' \
-d '{ "tier": "premium", "siteType": "local-business",
"niche": "dentistry", "language": "es", "country": "ES" }'
# → { "jobId": "V1StGXR8Z5jd", "queue": { "position": 3, … } }
# 2. Poll the status every 3–5 seconds
curl https://exstru.com/api/v1/status/V1StGXR8Z5jd \
-H 'Authorization: Bearer sg_your_key'
# 3. Download the archive when status = completed
curl -o site.zip https://exstru.com/api/v1/download/V1StGXR8Z5jd \
-H 'Authorization: Bearer sg_your_key'Access and the key
API access belongs to the team. A key like sg_… is issued by the owner in the bot's “API” section — where it's also reissued and revoked. Credits for sites are charged to the team's shared balance.
How to pass the key
In either of two headers — they are equivalent. The second one is for when your library already uses Authorization.
Authorization: Bearer sg_your_key X-Api-Key: sg_your_key
Tiers in the request
The tier is passed in the field tier and determines how many credits are charged and what the site gets. The API subscription doesn't restrict the tier: one key can order any of them.
basicpremiumeliteThe full feature comparison is on the Pricing page.
Order a site
Queues a site and charges credits by tier. The response comes right away — with the job ID and the place in the queue.
Request body parameters
Field rules
Format errors are returned all at once: 400 invalid_request, listed in data.issues. No credits are charged. Unknown fields are ignored.
companyInfoobjectIf the object is sent, name, email and phone are required. Without the object, the generator invents the name and contacts. All fields are strings.
brandColorstring- Format —
#rrggbbor a short#rgb, case-insensitive, surrounding spaces are trimmed.#1E6FD9and#1e6fd9— the same color,#16dreads as#1166dd. - An empty string or no field — the palette is picked for the niche.
- Anything else —
rgb(), color names, a code without#— error400 invalid_request. - The color isn't copied one-to-one: each site gets its own close shade, so one team's sites don't repeat each other.
- On Basic the field is accepted but not applied — warning
brand_color_not_in_tier.
baseUrlstring · up to 253 characters- A Latin-script domain: letters, digits and hyphens, at least one dot. Cyrillic domains — in punycode:
xn--… - The scheme is optional:
http://andhttps://are accepted; site links will always usehttps. wwwis kept — you decide which host is canonical. A path is allowed for a site in a subfolder.- A trailing slash, parameters
?…and an anchor#…are dropped. - Not allowed: spaces, a port, an IP address, credentials in the address (
user@) — error400 invalid_request. - Without a domain the site is built without
sitemap.xml,canonical,og:urlandhreflang: they need a full address.
extraLanguagesstring[]- Codes — same as in
language: two-letter lowercase ISO 639-1 codes from the supported list. Regional variants (en-GB,pt-BR) are not accepted. - The main language sits at the site root and is marked as
x-default. Additional languages go into subfolders/en/,/de/in array order. - Duplicates and the main language inside
extraLanguagesare skipped and don't count toward the limit. - Tier limit — Premium 2, Elite 9 additional, Basic 0. Extra languages are dropped from the end of the array and the order price doesn't change — warning
language_limit. - An unsupported code in
languageorextraLanguages— error400 invalid_request. - If the niche can't be done in one of the languages (for example, VPN in Russian) —
422 niche_language_refused, and no order is created. Remove that language and try again.
"tier": "premium",
"language": "es",
"extraLanguages": ["es", "en", "en", "de", "fr"]
→ site languages: es (root), /en/, /de/
→ "warnings": [{
"code": "language_limit",
"field": "extraLanguages",
"message": "Premium: up to 3 languages, fr not added"
}]countrystring · 2 letters- An ISO 3166-1 code, case-insensitive:
mxandMXare the same country. - Supported:
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. - The country does not change the site's language — that is set by
language. An English site for a German company is"language": "en", "country": "DE": prices in euros, phone+49, imprint in English. - Missing, or a code not on the list, is an error
400 invalid_requestwith the fieldcountry, no credits are charged.
"language": "es", "country": "MX" → prices in pesos → phone +52 55 1234 5678 → <html lang="es-MX">
archiveFormatstring · html | phphtml(default) — a static site: any hosting, Netlify, GitHub Pages.php— the same pages plus a request handler: forms send requests tocompanyInfo.email. Needs hosting with PHP 7.0 or newer where sending mail works (mail()).- Without
companyInfo.emailthe site gets demo contacts and there is no email for requests: forms accept none until you write an address at the top of the PHP file. - The price does not depend on the format. Any other value is an error
400 invalid_request.
Example
POST /api/v1/generate
Authorization: Bearer sg_your_key
Content-Type: application/json
{
"tier": "premium",
"siteType": "local-business",
"niche": "dentistry",
"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": []
}Response fields
Check status
Shows what stage the order is at. Poll every 3–5 seconds until the status is final: completed, failed or cancelled.
{
"jobId": "V1StGXR8Z5jd",
"status": "processing",
"queuePosition": null,
"etaSeconds": null,
"progress": 55,
"step": "Texts ready, picking photos…",
"error": null,
"ready": false
}Statuses
Cancel an order
Works while the order waits in the queue (pending). Credits go back to the balance immediately. Once the build has started it can't be cancelled — the response is 409 not_waiting.
{ "jobId": "V1StGXR8Z5jd", "status": "cancelled", "refunded": true }Download the archive
Returns application/zip as a stream when the status is completed. The archive is kept for 14 days and can be downloaded any number of times. After that the response is 410 gone.
Errors
The machine-readable error code is in data.code. Rely on it: the message text may change.
{
"statusCode": 422,
"message": "We do not generate this niche: …",
"data": {
"code": "niche_refused",
"category": "gambling",
"reason": "gambling",
"alternatives": [
{ "label": "board game club", "canonical": null }
]
}
}Another team's job returns the same 404 as a job that doesn't exist — so job IDs can't be probed by enumeration.
Limits and queue
With 429 comes with the header Retry-After with a number of seconds. Wait for it: retrying early doesn't get around the limit, it extends it. Limits are counted per key, not per IP address.