exstru
Open in Telegram
← API overview

exstru API · v1

Documentation API.

Everything you need to integrate: access and the key, order parameters, statuses, the archive, errors, limits and how credits are charged.

BASE URLhttps://exstru.com/api/v1
FORMATJSON · UTF-8
AUTHORIZATIONBearer sg_…
OUTPUTSite ZIP archive

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.

01Get a keyBuy the Team, Holding or Kraken pack — the API turns on for 30 days free. The key is issued in the bot, in the “API” section.
02Order a siteSend the niche, language and tier. The response has the job ID and its place in the queue.
03Get the archivePoll the status every 3–5 seconds. As soon as the order is ready, download the ZIP.
full cycle
# 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.

30 days freeWith your first Team, Holding or Kraken pack. Once per team. We'll remind you 7 and 3 days before it ends and on the last day.
$99 / 30 daysAfter the trial period. Paid from the stable dollar balance, not from credits.
Auto-renewalOnly you can turn it on. If there isn't enough money to renew, the owner gets a notification and the API keeps working for 24 more hours.
No subscriptionOnly the API turns off: the team workspace, credits and generation in the bot keep working. The key responds with 403 subscription_inactive.

How to pass the key

In either of two headers — they are equivalent. The second one is for when your library already uses Authorization.

headings
Authorization: Bearer sg_your_key
X-Api-Key: sg_your_key
The key is access to the team's credits.Keep it on your server, not in a browser or a mobile app: it's easy to extract from there. If the key leaks, revoke or reissue it in the bot — the old one stops working immediately.

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.

Parameterbasicpremiumelite
Credits charged61025
Languages per site1up to 3up to 10
Your own brand color—✓✓
Logo—uniqueunique
ImagesstockAI for the nicheAI for the niche
Formscontact+ callback request+ quiz
Core pages~5–7~7–10~10–15+
Blog articles3–55–1010–15

The full feature comparison is on the Pricing page.

POST/api/v1/generate

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

FieldDescription
tierrequiredstring · basic | premium | eliteThe site's tier: 6, 10 or 25 credits will be charged.
siteTyperequiredstring · local-business | blogA company website or a blog. For a blog the number of articles depends on the tier: 3–5, 5–10 or 10–15.
nicherequiredstring · 2–100 charactersThe niche in your own words, e.g. “dentistry” or “washing machine repair”. We only generate legitimate niches: others get a 422 niche_refused with options nearby, and no credits are charged.
languagerequiredstring · ISO 639-1 codeThe site's main language: a two-letter code, one of the 14 supported — en, de, ru, es, fr, it, pt, nl, pl, cs, ro, sv, da, no.
extraLanguagesoptionalstring[]Additional languages; the site gets a language switcher. Premium — up to 2, Elite — up to 9: up to 3 and 10 including the main one. Basic — main language only. Duplicates, limits and incompatible languages — in the rules below.
countryrequiredstring · ISO 3166-1 codeThe business's country: price currency, phone and address format, the region in the markup, and the legal pages it requires, such as the imprint for DE, AT and CH. Required. The list is in the rules below.
archiveFormatoptionalstring · html | phpArchive format: html — a static site for any hosting, php — forms send requests to email by themselves. Without it — html. More in the rules below.
baseUrloptionalstring · domainThe future site's domain: with it the archive includes sitemap.xml, canonical and hreflang. Format — in the rules below.
brandColoroptionalstring · #rrggbbBrand color — on Premium and Elite; the palette is built around it. Format — in the rules below.
keywordsoptionalstring[] · up to 8 phrasesKey phrases that go into headings and texts.
companyInfooptionalobjectCompany name, contacts and facts. If you don't send them, the generator invents them; the more data, the more accurate the texts. Schema — in the rules below.

Field rules

Format errors are returned all at once: 400 invalid_request, listed in data.issues. No credits are charged. Unknown fields are ignored.

companyInfoobject

If the object is sent, name, email and phone are required. Without the object, the generator invents the name and contacts. All fields are strings.

name2–200 · requiredCompany name, as on the sign.
emailemail · requiredAddress for the contact page and forms.
phone5–30 · requiredPhone in international format: +34 600 000 000.
addressup to 300Office or store address.
descriptionup to 1000What the company does: 1–3 sentences.
legalInfoup to 500Legal name and company details for the legal pages.
foundedYearup to 20Year founded: “2014” or “since 2014”.
teamSizeup to 20Team size: “12” or “10–15”.
founderNameup to 120Founder's name for the About page.
founderRoleup to 120Founder's role.
serviceAreaup to 160Service area: “Madrid and suburbs”.
brandColorstring
  • Format — #rrggbb or a short #rgb, case-insensitive, surrounding spaces are trimmed. #1E6FD9 and #1e6fd9 — the same color, #16d reads as #1166dd.
  • An empty string or no field — the palette is picked for the niche.
  • Anything else — rgb(), color names, a code without # — error 400 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:// and https:// are accepted; site links will always use https.
  • www is 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@) — error 400 invalid_request.
  • Without a domain the site is built without sitemap.xml, canonical, og:url and hreflang: they need a full address.
InputResult
Example.COM/https://example.com
http://www.example.comhttps://www.example.com
https://example.com/clinic/?utm=1https://example.com/clinic
xn--80aswg.xn--p1aihttps://xn--80aswg.xn--p1ai
example.com:8080400 invalid_request
мой сайт.рф400 invalid_request
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 extraLanguages are 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 language or extraLanguages — error 400 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.
example · premium
"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: mx and MX are 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_request with the field country, no credits are charged.
example
"language": "es",
"country": "MX"

→ prices in pesos
→ phone +52 55 1234 5678
→ <html lang="es-MX">
archiveFormatstring · html | php
  • html (default) — a static site: any hosting, Netlify, GitHub Pages.
  • php — the same pages plus a request handler: forms send requests to companyInfo.email. Needs hosting with PHP 7.0 or newer where sending mail works (mail()).
  • Without companyInfo.email the 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

request
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"
  }
}
response · 200
{
  "success": true,
  "jobId": "V1StGXR8Z5jd",
  "queue": {
    "position": 3,
    "etaSeconds": 90
  },
  "warnings": []
}

Response fields

jobIdJob ID: needed for status, cancellation and download.
queuePlace in the queue (position, 1 = next) and the estimated wait before start in seconds (etaSeconds). null if the build started right away.
warningsWhat from the request wasn't applied — the order still runs. An array of { code, field, message } objects: language_limit — languages beyond the tier, brand_color_not_in_tier — a brand color on Basic.
GET/api/v1/status/{jobId}

Check status

Shows what stage the order is at. Poll every 3–5 seconds until the status is final: completed, failed or cancelled.

statusOne of five statuses — see below.
queuePositionPlace in the queue while the order waits. Otherwise null.
etaSecondsEstimated wait before start, while the order is waiting.
progressProgress from 0 to 100.
stepThe current build stage as text.
errorThe reason, if the build failed.
readytrue — the archive can be downloaded.
response · 200
{
  "jobId": "V1StGXR8Z5jd",
  "status": "processing",
  "queuePosition": null,
  "etaSeconds": null,
  "progress": 55,
  "step": "Texts ready, picking photos…",
  "error": null,
  "ready": false
}

Statuses

pendingWaiting in the queue. Can be cancelled.
processingBuilding.
completedReady — download the archive.
failedThe build failed. Credits were returned automatically.
cancelledCancelled in the queue. Credits returned.
POST/api/v1/cancel/{jobId}

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.

response · 200
{ "jobId": "V1StGXR8Z5jd", "status": "cancelled", "refunded": true }
GET/api/v1/download/{jobId}

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.

HTML · CSS · imagesA ready static site for any hosting — no CMS, no build step.
robots.txt · 404 pageThe technical minimum — on every tier.
sitemap.xml · canonicalIf the order has a domain in baseUrl.
Legal pagesIn the site's languages — on every tier.

Errors

The machine-readable error code is in data.code. Rely on it: the message text may change.

example · 422
{
  "statusCode": 422,
  "message": "We do not generate this niche: …",
  "data": {
    "code": "niche_refused",
    "category": "gambling",
    "reason": "gambling",
    "alternatives": [
      { "label": "board game club", "canonical": null }
    ]
  }
}
HTTPdata.codeWhat it means and what to do
400invalid_requestThe request failed validation. All problems at once — in data.issues.
401no_keyNo key was sent. Add an Authorization or X-Api-Key header.
401unknown_keyThe key is wrong. Check it against the key in the bot.
402insufficient_creditsNot enough credits on the team balance. Top up the balance or turn on auto top-up.
403key_blockedThe key has been revoked. Issue a new one in the bot.
403subscription_inactiveThe API subscription is inactive. The owner renews it in the bot — $99 per 30 days.
404not_foundThe job doesn't exist or belongs to another team.
409not_waitingOnly a queued order can be cancelled — this one is already building or done.
409not_readyThe order isn't ready yet. Wait for the completed status.
410goneThe archive is no longer stored: 14 days have passed.
422niche_refusedWe don't generate this niche. The reason is in data.category and data.reason, up to 3 legitimate niches nearby — in data.alternatives. No credits are charged.
422niche_language_refusedWe do this niche, but not in this language. Remove the language from language or extraLanguages. No credits are charged.
429queue_full_for_accountThe team already has 30 orders in the queue. A new one will be accepted once some of them are built. No credits are charged.
429rate_limitedToo many requests. Wait as many seconds as Retry-After says.
503niche_unverifiedThe niche couldn't be checked. Repeat the request — no credits are charged.

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

5 per minuteorders per key.
240 per minuteother requests: status, cancel, download.
30 in the queueorders per team at once. New ones are accepted once some are built.
Fair queueOne team's orders don't hold up the others: the response always shows place and wait time.

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.

Charges and refunds

When credits are chargedWhen the order joins the queue, before the build starts: 6, 10 or 25 credits by tier. The API calls themselves are free.
FailureIf a build fails, credits are returned automatically.
CancelWhile the order is in the queue you can cancel it — credits come back immediately.
No chargeA declined niche, a request error or a full queue — no credits are charged.
No double chargesCharges and refunds are tied to the job ID: one order can't be charged or refunded twice.
Bonus creditsIf there are bonus credits on the balance, they are spent first.
Purchased creditsThey stay on the balance with no time limit.
Auto top-upFor teams: when credits drop below a set threshold, the chosen pack is bought from the stable balance automatically, so the integration doesn't stop. Off by default.
Need help with integration?Write to support [@username] — we'll help with code and scenarios.Get a key in TelegramAPI overview