CloudArch HTTP API
Overview
The CloudArch REST API exposes the same surface used by the web app: scripts, comments, collections, profiles, quotas, billing, and account self-service. All requests are made over HTTPS to https://api.cloud-arch.ru and accept and return application/json. All resource identifiers are UUIDs unless noted.
This is a stable /v1 surface. Breaking changes ship under a new prefix.
Authentication
Generate an API key from /workspace/api and pass it as a Bearer token on every request:
Authorization: Bearer ca_<your-key>Each key is rate limited to 1000 requests per hour. Keys are user-scoped: a request authenticated with your key acts as you and is bound by your tier limits, quotas, and ownership rules.
When calling from the web origin you can also rely on the session cookie (credentials: 'include'). The Bearer token takes precedence when both are present.
Quickstart
List public scripts in the gallery:
curl -H "Authorization: Bearer ca_..." \
https://api.cloud-arch.ru/v1/scripts/gallerySuccessful responses look like { "items": [...], "totalCount": <int> }. Most endpoints return a single resource as { "item": {...} } and a write-only acknowledgement as { "ok": true }.
Scripts
GET /v1/scripts/gallery
Search and paginate the public + template gallery. Optional query: query, category, tag, page (default 1), pageSize.
curl "https://api.cloud-arch.ru/v1/scripts/gallery?query=kafka&page=1&pageSize=20"{
"items": [
{
"id": "8c7e...",
"name": "Kafka cluster",
"slug": "kafka-cluster",
"description": "Three-broker Kafka with ZooKeeper",
"type": "topology",
"category": "Infrastructure",
"isPublic": true,
"viewCount": 421,
"starCount": 7,
"tags": ["kafka", "messaging"]
}
],
"totalCount": 1
}GET /v1/scripts/:id
Fetch one script by UUID or by slug. Returns 404 if the script is private and the requester is not the owner.
curl -H "Authorization: Bearer ca_..." \
https://api.cloud-arch.ru/v1/scripts/kafka-cluster{
"item": {
"id": "8c7e...",
"name": "Kafka cluster",
"code": "const topology = new TopologyBuilder(true);\n...",
"type": "topology",
"isPublic": true
}
}POST /v1/scripts
Create a new script. Subject to draftsPerDay quota; publishPerDay is checked separately if isPublic is true. Topology scripts are validated server-side; validation failures return 422.
curl -X POST -H "Authorization: Bearer ca_..." \
-H "Content-Type: application/json" \
-d '{"name":"My diagram","type":"topology","code":"const t = new TopologyBuilder(true);","isPublic":false}' \
https://api.cloud-arch.ru/v1/scripts{
"item": {
"id": "f4d2...",
"name": "My diagram",
"type": "topology",
"isPublic": false
}
}PATCH /v1/scripts/:id
Update fields on a script you own. Body keys are optional; flipping isPublic: true re-checks the publish quota and stamps publishedAt.
curl -X PATCH -H "Authorization: Bearer ca_..." \
-H "Content-Type: application/json" \
-d '{"description":"Updated copy","isPublic":true}' \
https://api.cloud-arch.ru/v1/scripts/f4d2...{ "item": { "id": "f4d2...", "isPublic": true, "publishedAt": "2026-04-30T10:00:00.000Z" } }DELETE /v1/scripts/:id
Permanently delete a script you own.
curl -X DELETE -H "Authorization: Bearer ca_..." \
https://api.cloud-arch.ru/v1/scripts/f4d2...{ "ok": true }POST /v1/scripts/:id/star
Toggle a star on a script. Returns the new state.
curl -X POST -H "Authorization: Bearer ca_..." \
https://api.cloud-arch.ru/v1/scripts/8c7e.../star{ "starred": true }POST /v1/scripts/:id/view
Increment the view counter. No auth required. Use sparingly: this is intended for embed/viewer pages, not crawlers.
curl -X POST https://api.cloud-arch.ru/v1/scripts/8c7e.../view{ "ok": true, "viewCount": 422 }Comments
GET /v1/scripts/:id/comments
List comments on a script. Optional query: targetType (script | node | edge) and targetId to filter to a specific node/edge anchor.
curl "https://api.cloud-arch.ru/v1/scripts/8c7e.../comments?targetType=node&targetId=broker-1"{
"items": [
{
"id": "a91b...",
"scriptId": "8c7e...",
"userId": "u-123",
"content": "Why three brokers and not five?",
"targetType": "node",
"targetId": "broker-1",
"createdAt": "2026-04-29T08:21:00.000Z",
"author": { "username": "ada", "fullName": "Ada Lovelace", "avatarUrl": null }
}
]
}POST /v1/scripts/:id/comments
Add a comment. Subject to commentsPerHour quota. targetType is required; targetId is required for node and edge targets and ignored for script.
curl -X POST -H "Authorization: Bearer ca_..." \
-H "Content-Type: application/json" \
-d '{"content":"Nice diagram!","targetType":"script"}' \
https://api.cloud-arch.ru/v1/scripts/8c7e.../comments{ "item": { "id": "a91c...", "content": "Nice diagram!", "targetType": "script" } }DELETE /v1/comments/:commentId
Delete one of your own comments. The user-scoped variant is preferred over the legacy DELETE /v1/scripts/:id/comments/:commentId — both work.
curl -X DELETE -H "Authorization: Bearer ca_..." \
https://api.cloud-arch.ru/v1/comments/a91c...{ "ok": true }Collections
Collections are private folders that group scripts owned or starred by the user.
GET /v1/collections
List your collections, including the script count for each.
curl -H "Authorization: Bearer ca_..." \
https://api.cloud-arch.ru/v1/collections{
"items": [
{ "id": "c1...", "name": "Messaging", "color": "#7c3aed", "scriptCount": 3, "createdAt": "2026-04-01T00:00:00.000Z" }
]
}POST /v1/collections
Create a new collection. Body: { name, description?, color? }.
curl -X POST -H "Authorization: Bearer ca_..." \
-H "Content-Type: application/json" \
-d '{"name":"Messaging","color":"#7c3aed"}' \
https://api.cloud-arch.ru/v1/collections{ "item": { "id": "c1...", "name": "Messaging", "scriptCount": 0 } }POST /v1/collections/:id/scripts
Attach a script to a collection. Body: { scriptId }. The script must be owned by you or public.
curl -X POST -H "Authorization: Bearer ca_..." \
-H "Content-Type: application/json" \
-d '{"scriptId":"8c7e..."}' \
https://api.cloud-arch.ru/v1/collections/c1.../scripts{ "ok": true }DELETE /v1/collections/:id/scripts/:scriptId
Remove a script from a collection. The script itself is not deleted.
curl -X DELETE -H "Authorization: Bearer ca_..." \
https://api.cloud-arch.ru/v1/collections/c1.../scripts/8c7e...{ "ok": true }Profiles
GET /v1/profiles/by-username/:username
Public profile lookup. Returns 404 if no profile with that username exists.
curl https://api.cloud-arch.ru/v1/profiles/by-username/ada{
"item": {
"id": "u-123",
"username": "ada",
"fullName": "Ada Lovelace",
"avatarUrl": null,
"createdAt": "2026-01-04T11:30:00.000Z"
}
}Quota
GET /v1/quota/me
Current usage and effective limits for the authenticated user. Used by the dashboard widget. Returns tierId, isAdmin, and an array of { key, used, limit, windowSeconds } entries covering publishPerDay, draftsPerDay, commentsPerHour, and aiChatPerDay.
curl -H "Authorization: Bearer ca_..." \
https://api.cloud-arch.ru/v1/quota/me{
"tierId": "free",
"isAdmin": false,
"quotas": [
{ "key": "publishPerDay", "used": 1, "limit": 5, "windowSeconds": 86400 },
{ "key": "draftsPerDay", "used": 4, "limit": 20, "windowSeconds": 86400 },
{ "key": "commentsPerHour", "used": 0, "limit": 30, "windowSeconds": 3600 },
{ "key": "aiChatPerDay", "used": 0, "limit": 10, "windowSeconds": 86400 }
]
}Billing
Billing endpoints require authentication, except the two read-only ones below (/v1/billing/status and /v1/billing/bundles) — a paywall has to name its price before the visitor has an account. The payment provider is deployment-configured (PAYMENT_PROVIDER): T-Bank acquiring or CloudPayments. A 503 response means billing is not configured for the current deployment.
The provider decides the shape of a checkout response, so read it by which field is present — paymentUrl (T-Bank: redirect the browser there) or widget (CloudPayments: parameters for the in-page widget). Never assume one of them.
GET /v1/billing/status
Whether this deployment accepts payments at all. No authentication. Clients use it to render an “unavailable” state instead of a checkout CTA that would 503 after the click.
curl https://api.cloud-arch.ru/v1/billing/status{ "configured": true }GET /v1/billing/bundles
The sellable one-off bundles and their prices. No authentication required.
With a session, prices are quoted for that user: a checkout created before a price change is reused as-is by POST /v1/billing/purchase-bundle (re-minting an invoice is how a buyer ends up charged twice), so this endpoint returns the pending amount rather than the live price. Without a session, the live price list.
curl https://api.cloud-arch.ru/v1/billing/bundles{
"items": [
{
"id": "system-design-cases",
"name": "System Design Cases",
"description": "System Design Cases — все кейсы интервью по системному дизайну",
"priceKopeks": 99000,
"currency": "RUB"
}
]
}GET /v1/billing/me
Return the user’s active subscription (if any), the matching plan, and the 50 most recent payments.
curl -H "Authorization: Bearer ca_..." \
https://api.cloud-arch.ru/v1/billing/me{
"subscription": { "id": "s1...", "planId": "premium-monthly", "status": "active", "cancelAtPeriodEnd": false },
"plan": { "id": "premium-monthly", "priceKopeks": 49900, "intervalDays": 30 },
"payments": [
{ "id": "p1...", "amountKopeks": 49900, "status": "succeeded", "createdAt": "2026-04-01T00:00:00.000Z" }
]
}POST /v1/billing/subscribe
Start a subscription. Body: { planId, consent }. Returns the new subscription id, the payment row id and the provider-specific checkout (see above). Returns 409 already_subscribed when an active subscription already exists. Re-clicking the same plan while a checkout is still pending reuses that pending payment instead of creating a second one.
consent is required and records agreement to recurring charges: { acceptedRecurring: true, recurringPolicyVersion, locale, path? }. acceptedRecurring must be literally true — a missing or false value is a 400 and no subscription or payment row is created. The consent is stored with the user id, the timestamp and the policy version before any payment is created.
curl -X POST -H "Authorization: Bearer ca_..." \
-H "Content-Type: application/json" \
-d '{"planId":"premium-monthly"}' \
https://api.cloud-arch.ru/v1/billing/subscribe{
"subscriptionId": "s1...",
"paymentId": "p1...",
"paymentUrl": "https://securepay.tinkoff.ru/new/...",
"orderId": "11111111-1111-4111-8111-111111111111"
}POST /v1/billing/promo/validate
Preview a promo-code discount before paying. Body: { code, target, planId? , bundleId? } where target is subscription or bundle. Answers 200 in both cases — { valid: true, ... } with the computed amounts, or { valid: false, reason } where reason is one of not_found, inactive, not_started, expired, exhausted, wrong_target, already_used.
This is a preview, not a reservation: a code can run out between the preview and the payment, so the authoritative check happens during checkout.
{
"valid": true,
"code": "LAUNCH50",
"discountType": "percent",
"discountValue": 50,
"originalAmountKopeks": 29900,
"discountKopeks": 14950,
"finalAmountKopeks": 14950
}Both POST /v1/billing/subscribe and POST /v1/billing/purchase-bundle accept an optional promoCode. The discount applies to that payment only — a subscription keeps charging the full plan price on renewal. A rejected code answers 400 { error, reason } and creates no payment. Re-clicking checkout while a pending payment exists reuses that payment (discount included) and does not spend the code twice.
POST /v1/billing/consent/revoke
Withdraw consent for automatic payments. Separate from cancelling the subscription: besides turning auto-renewal off it deletes the stored card token, so nothing can be charged even if a flag is later lost. Access stays until the end of the paid period. Returns 404 when the user has no active subscription.
curl -X POST -H "Authorization: Bearer ca_..." \
https://api.cloud-arch.ru/v1/billing/consent/revoke{
"subscription": { "id": "s1...", "cancelAtPeriodEnd": true, "status": "active" },
"recurringConsent": null
}Under CloudPayments the same endpoint answers with a widget object instead of paymentUrl/orderId.
PATCH /v1/billing/subscription
Toggle cancelAtPeriodEnd on the active subscription. The subscription stays active until the period ends; set the flag back to false to resume.
curl -X PATCH -H "Authorization: Bearer ca_..." \
-H "Content-Type: application/json" \
-d '{"cancelAtPeriodEnd":true}' \
https://api.cloud-arch.ru/v1/billing/subscription{ "subscription": { "id": "s1...", "cancelAtPeriodEnd": true, "status": "active" } }Account
DELETE /v1/account
Permanently delete the authenticated user. Cascades remove sessions, profiles, scripts, stars, comments, collections, AI usage, quota overrides, subscriptions, and payments. Audit-event rows survive with the actor email snapshot. This action cannot be undone.
curl -X DELETE -H "Authorization: Bearer ca_..." \
https://api.cloud-arch.ru/v1/account{ "ok": true }Reports
POST /v1/reports
File a content report against a public script. Body: { targetType: "script", targetId, reason }. The reason must be 10-2000 characters.
curl -X POST -H "Authorization: Bearer ca_..." \
-H "Content-Type: application/json" \
-d '{"targetType":"script","targetId":"8c7e...","reason":"This diagram contains spam links in the description."}' \
https://api.cloud-arch.ru/v1/reports{ "ok": true, "id": "r1..." }Access tiers and content gating
Scripts carry a section (explore | cases | concepts) and an accessTier
(free | premium). Every read path that would return code or readme runs the
access gate first, so a premium case is visible to everyone but readable only to
callers who are entitled to it.
A caller is entitled when any of these hold:
- the script is not in the
casessection (exploreandconceptsare always free), or accessTierisfree, or- the caller is the script’s creator, or
- the caller is a member of the workspace that owns the script, or
- the caller owns the bundle derived from the script’s
category:<slug>tag (category:system-design→ bundlesystem-design-cases).
A premium case whose category tag is missing or unrecognised fails closed.
When the gate denies access the response is not a 403 — it is a normal 200 with the
paid fields emptied:
{
"item": {
"id": "8c7e...",
"name": "Design Payment System",
"accessTier": "premium",
"code": "",
"readme": null,
"readmePreview": "## Requirements\n\n...",
"shareToken": null,
"userCanAccess": false
}
}Always branch on userCanAccess rather than on the presence of code. shareToken is
withheld too, so an ungated caller cannot resolve the script through
GET /v1/share/:token.
readmePreview is the mirror image of readme: it carries a bounded teaser only
when access is denied, and is null for callers who can read readme itself. The
teaser stops at the third ## heading (so the first two sections stay whole) and is
capped at 2200 characters and 45% of the readme, whichever cuts first. If the cut
leaves nothing but a heading, the field is null rather than an empty-looking card.
Endpoints not covered on this page
This page documents the core content surface. The /v1 API is larger; the rest is stable
but undocumented here, and the shapes are best read off the route files in
apps/api/src/routes/.
| Area | Endpoints |
|---|---|
| Script discovery | GET /v1/scripts/search, /public, /featured, /templates, /categories, /tags, /starred, /by-slugs, /by-user/:userId, /user |
| Versions | GET / POST /v1/scripts/:id/versions, POST /v1/scripts/:id/versions/:versionNumber/restore |
| Sharing | POST / DELETE /v1/scripts/:id/share-token, GET /v1/share/:token, POST /v1/scripts/:id/thumbnail |
| Pins | GET /v1/me/pins, POST / DELETE /v1/scripts/:id/pin |
| Schemas (ER diagrams) | POST / GET /v1/schemas, GET / PATCH / DELETE /v1/schemas/:id |
| Workspaces | POST / GET /v1/workspaces, GET / PATCH / DELETE /v1/workspaces/:id, /public, /avatar, /transfer-ownership, members and invitations |
| Learning | GET /v1/courses/:slug, GET /v1/scripts/:id/lesson-context, POST /v1/concepts/:slug/viewed, GET /v1/concepts/mastered |
| Notifications & reminders | GET /v1/notifications, POST /v1/notifications/read-all, GET / PATCH /v1/reminders/schedule, /pause, /resume |
| Analytics | POST /v1/analytics/events, /funnel, GET /v1/analytics/dashboard, /popular, /recent, /script/:id/summary, /script/:id/daily |
| AI | GET / POST /v1/ai/chat, POST /v1/ai/realtime/session, /usage |
| Account | POST /v1/account/consents, GET / PATCH /v1/account/preferences, GET /v1/me/activity, /me/ai-suggestions |
Admin (requires is_admin) | everything under /v1/admin/* — scripts, users, courses, bundles, reports, emails, stats, LLM config, refunds — plus GET /v1/audit/events |
Rate limits and quotas
Two layers of throttling apply:
- Per-key rate limit: 1000 requests per hour per API key. Exceeded requests are rejected by the auth layer.
- Per-user quotas: each plan tier has separate caps for daily publishes, daily drafts, hourly comments, and daily AI chat calls. See
GET /v1/quota/mefor the current windows. Exceeding a quota returns429 quota_exceeded. See the pricing page for the per-tier numbers.
Errors
All errors are JSON. Common shapes:
| Status | When |
|---|---|
401 | Missing or invalid credentials. |
403 | Authenticated but not allowed (wrong owner, non-admin route, non-public script). |
404 | Resource not found, or hidden from this requester. |
409 | Conflict, e.g. already_subscribed when starting a second subscription. |
413 | payload_too_large — script code or comment exceeds the per-tier byte limit. |
422 | Validation error. Body is a Zod issue list, or for topology scripts a structured DSL validation result. |
429 | { "error": "quota_exceeded", "quota": "<key>", "limit": <number> }. |
503 | Provider unavailable (e.g. billing_not_configured, S3 not configured for thumbnails). |
A typical Zod 422 response:
{
"error": "validation_failed",
"issues": [
{ "path": ["name"], "message": "String must contain at least 1 character(s)" }
]
}