SolveHub

API reference

SolveHub REST API Reference: Tasks, Prices, Balance, Webhooks

Complete SolveHub customer API reference: authentication with Bearer API keys, task creation and polling, live prices, balance and ledger, support tickets, webhooks and every error code.

For authorized QA automation and browser integration. Use only on websites and workflows you have permission to test.

This document is based on the current HTTP routes, application services, and task parameter validation. The production base URL is https://www.tiam.codes, and all paths below are relative to it; the local default is http://127.0.0.1:8080, and production must use HTTPS. All keys, email addresses, and domains in the examples are placeholders. Response times use RFC 3339, and amounts are integer USD micro-USD (micros): 1 USD = 1,000,000 micros. Live values from GET /v1/prices are authoritative for prices, and the sale price is frozen for each task at creation time.

General Conventions

Regular API calls use Authorization: Bearer <API_KEY>; JSON write requests use Content-Type: application/json. The customer web portal uses the solvehub_portal cookie with HttpOnly and SameSite=Strict; non-GET/HEAD requests authenticated by cookie must include X-SolveHub-Portal: 1. Login, registration, and logout requests also require this header. Bearer takes precedence over the cookie: do not let the browser unintentionally attach an Authorization header for another Key.

Every response carries X-Request-ID, which support can use to locate an issue. Failure responses are usually {"error":"ERROR_CODE"}, not the success response format; 204 has no response body. The request body accepts only a single JSON object, and unknown top-level fields usually return 400 INVALID_JSON. JSON request bodies are limited to 1 MiB, except task creation and the compatibility endpoints, which allow up to 8 MiB.

HTTP status Common meaning and handling
400 Parameter error, such as INVALID_JSON, INVALID_TASK, INVALID_IDEMPOTENCY_KEY; fix the request.
401 UNAUTHORIZED, API_KEY_EXPIRED; account login may also return TOTP_REQUIRED or TOTP_INVALID.
402 INSUFFICIENT_BALANCE; top up first or adjust spending.
403 INSUFFICIENT_SCOPE, IP_NOT_ALLOWED, ACCOUNT_FROZEN, ACCOUNT_CLOSED, ACCOUNT_UNBOUND (the email account has not yet bound a Key), PORTAL_CSRF_CHECK_FAILED, KEY_SPEND_FROZEN, KEY_QUOTA_EXHAUSTED (this Key's total quota is used up; refunds do not restore quota, so contact the platform to adjust it or use another Key); a paid renewal using an expired Key returns RENEWAL_REAUTH_REQUIRED.
404 A resource that is not visible to the current customer or does not exist; do not use it to infer whether another customer's resource exists.
409 State/version or idempotency conflict, such as IDEMPOTENCY_CONFLICT, VERSION_CONFLICT.
410 The retired legacy endpoints for creating top-up orders and payment instructions.
429 Quota, support ticket rate, or login limits; KEY_SPEND_LIMIT means the current Key's 24-hour safety limit is exhausted. The response carries Retry-After: 300, but the actual recovery time depends on when historical holds leave the window. KEY_CONCURRENCY_LIMIT means this Key already has as many queued or processing tasks as the concurrency limit the platform set for it; submit again after existing tasks finish. The response carries Retry-After: 60.
500/503 Internal failure or unavailable dependency; when you have an idempotency key, retry with the same request and the same key, and first query any resource that may already have been created. Transient database failures are retried automatically by the server; if not recovered after about 10 seconds, 503 DATABASE_BUSY with Retry-After: 1 is returned: read requests can be retried directly; write requests may already have taken effect (when the commit result cannot be confirmed), so retry with the same idempotency key or query first.

API Key scopes are tasks (tasks, usage), billing (balance, ledger, recharge Keys, historical orders), and account (account profile, Key metadata, Webhooks, session list). Creating, modifying, or revoking Keys, changing the contact email, and ending sessions on other devices additionally require an email/password account session; even a Key with the account scope cannot perform these sensitive changes. Some grace endpoints accept expired Keys, such as balance, recharge Key redemption, renewal records, and support tickets; an expired Key cannot create tasks, nor can it authorize a paid renewal with its own credentials. A frozen account cannot run new tasks, redeem recharge Keys, or perform similar operations. Support tickets use the authenticated customer identity and have no separate scope.

The unauthenticated GET /healthz returns {"status":"ok"}, meaning the API process is alive; GET /readyz returns {"status":"ready"}, meaning the database and migration markers are available, and returns 503 DATABASE_UNAVAILABLE on failure. Neither endpoint guarantees that task processing capacity is available.

Identity and Sessions

Method and path Request Success response
POST /v1/portal/register/email No login required, X-SolveHub-Portal: 1; {"email":"ops@example.com"} 204, the code is queued in the mail outbox. Registration requires email verification first; the code is valid for 20 minutes and can be used once, requesting a new one invalidates the old code, and five failed attempts discard it. An already registered email returns 409 ACCOUNT_EXISTS; too many requests from one source return 429 REGISTER_THROTTLED.
POST /v1/portal/register No login required, X-SolveHub-Portal: 1; {"email":"ops@example.com","password":"...","code":"..."}, where code comes from the previous endpoint 201 {"customerId":"uuid","status":"unbound"}; the sign-in email is marked verified immediately, so unverified accounts are never created. The new account is in the unbound state: it has no Key and cannot view the balance or create tasks; log in with the account and then call bind-key. A wrong or expired code returns 400 EMAIL_CODE_INVALID; a non-compliant password returns 400 INVALID_ACCOUNT; too many registrations from the same source return 429 REGISTER_THROTTLED (with Retry-After).
POST /v1/portal/account/bind-key Account session, X-SolveHub-Portal: 1; {"apiKey":"sh_..."} 200 {"customerId":"uuid","keyId":"uuid","merged":true}. Unbound account: the account (login email, password, TOTP, current session) is attached as a whole to the customer that owns the Key, merged:true, with no need to log in again; if that customer already has an email account, 409 KEY_ALREADY_BOUND is returned. Bound account: only Keys of the same customer can be pasted, which stores the encrypted plaintext of an old Key so it can be viewed later, merged:false; another customer's Key returns 409 KEY_OWNED_BY_OTHER_CUSTOMER. A nonexistent or revoked Key returns 404 KEY_NOT_FOUND; failures are counted per source, and too many return 429 BIND_THROTTLED.
POST /v1/portal/session {"apiKey":"sh_..."}, plus X-SolveHub-Portal: 1 200 session view and sets the cookie. A Key session inherits the original Key's scopes, IP allowlist, and expiry restrictions.
POST /v1/portal/session/account {"email":"ops@example.com","password":"..."}; when TOTP is enabled, submit the same email and password again with code (a 6-digit one-time code or a single-use recovery code) 200 session view and sets the cookie. The password is checked first; 401 TOTP_REQUIRED is returned only if the password is correct and TOTP is enabled; otherwise login succeeds directly. A recovery code becomes invalid immediately after a successful login.
GET /v1/portal/session / DELETE /v1/portal/session Cookie; logout requires X-SolveHub-Portal: 1 Session view / 204.
GET /v1/portal/sessions / DELETE /v1/portal/sessions/{id} Listing requires the account scope; ending a session also requires an email/password account session {"sessions":[...]} / 204.
GET /v1/portal/account/security account scope {"registered":true,"totpEnabled":false}; account sessions additionally return email,emailVerified; Key sessions do not return the login email.
POST /v1/portal/account/claim Key session with the account scope; {"email":"...","password":"..."} 204; the Key holder binds it themselves, with no administrator activation code needed. Each customer can bind only once; submitting again returns 409 ACCOUNT_EXISTS; an email already in use also returns 409; a non-compliant password returns 400 INVALID_ACCOUNT; a closed customer returns 403 ACCOUNT_CLOSED.
POST /v1/portal/account/email-verification Account session, X-SolveHub-Portal: 1; empty JSON {} 204; sends a one-time verification code to the login email. Returns 503 EMAIL_UNAVAILABLE when SMTP is not configured; at least 15 minutes between requests per customer.
POST /v1/portal/account/email-verification/confirm Account session; {"code":"code from the email"} 204; the code is valid for 20 minutes and can be used only once. The notification email may differ from the login email; the verification status belongs only to the login email.
POST /v1/portal/contact-email/verification Account session, X-SolveHub-Portal: 1; empty JSON {} 204; sends a one-time verification code to the current notification email. Returns 503 EMAIL_UNAVAILABLE when SMTP is not configured; 409 EMAIL_ALREADY_VERIFIED if already verified; at least 15 minutes between requests per customer.
POST /v1/portal/contact-email/verification/confirm Account session; {"code":"code from the email"} 204; the notification email is verified. The code is valid for 20 minutes and can be used only once; changing the notification email invalidates old codes. An invalid code returns 400 EMAIL_CODE_INVALID.
POST /v1/portal/account/password-recovery No login required, X-SolveHub-Portal: 1; {"email":"ops@example.com"} Always 204, without revealing whether the account exists or whether the email is verified; only a verified login email receives the message. 503 when SMTP is not configured.
POST /v1/portal/account/password-recovery/confirm No login required, X-SolveHub-Portal: 1; {"email":"...","token":"code from the email","password":"new password","code":"one-time code or recovery code"} 204; code is required when 2FA is enabled. Both the email code and the recovery code are single-use; on success all account sessions are invalidated, while API Keys are unchanged. Invalid codes uniformly return 400 RECOVERY_INVALID.
POST /v1/portal/account/totp {"password":"..."} 200 {"secret":"...","uri":"otpauth://..."}; store it securely and never write it to logs.
POST /v1/portal/account/totp/confirm Account session; {"code":"123456"} 200 {"recoveryCodes":["SHRC-...",...]}; on successful verification, two-step verification is enabled immediately and 8 recovery code digests are saved atomically. The plaintext is returned only this once and should be stored offline.
POST /v1/portal/account/totp/recovery-codes Account session; {"password":"...","code":"123456"}, which must be a one-time code not yet used 200 {"recoveryCodes":[...]}; atomically invalidates all old recovery codes; the plaintext of the new codes is returned only this once.
POST /v1/portal/account/totp/disable Account session; {"password":"...","code":"123456"}; an unused recovery code is also accepted 204; all remaining recovery codes are invalidated immediately.
POST /v1/portal/account/password {"oldPassword":"...","newPassword":"...","code":"123456"}; omit code when TOTP is not enabled 204; existing account sessions are revoked; log in again.

Registration and binding flow: POST /v1/portal/register/email requests an email code → POST /v1/portal/register registers the email/password account with that code → POST /v1/portal/session/account logs in → POST /v1/portal/account/bind-key pastes the API Key issued by the operator, and the account then belongs to that Key's customer. One account corresponds to one customer and can manage that customer's multiple Keys; each customer can bind only one email account. While unbound, all endpoints other than session, security settings, and binding endpoints return 403 ACCOUNT_UNBOUND. Customers who already have a Key but no email account can still log in with the Key and bind an email on the "Security" page (see account/claim).

Passwords must be 12–128 Unicode characters, contain at least 6 distinct characters, and must not contain the email address; mixing in special characters is not required. The session view contains authMethod,sessionId,customerId,customerName,customerStatus,keyId,keyLabel,scopes,keyExpiresAt,keyExpired,expiresAt,idleTimeoutSeconds; authMethod is account for account sessions and api_key for Key sessions. Sessions expire 24 hours after sign-in. An account session is independent of any particular API Key; a Key session becomes invalid when that Key is revoked. A TOTP code cannot be reused within the same time step. Failed logins are limited per IP and per account; when you receive 429 LOGIN_THROTTLED, honor Retry-After. The login email of existing accounts is unverified by default and must be verified on the console "Security" page before self-service password recovery is possible. If you lose your authenticator, you can use a single-use recovery code to log in, disable 2FA, or reset your password together with an email code sent to the verified login email; if the recovery codes are also lost, contact an administrator for identity verification. Recovery codes are visible only when enabling or regenerating them, and should not be pasted into support tickets, chats, or logs.

Handling a leaked Key: If you still hold a password account session, use POST /v1/portal/keys/{id}/compromise (account scope) to revoke the leaked Key in an emergency; this endpoint can revoke the last Key, whereas the regular DELETE /v1/keys/{id} cannot. Then review GET /v1/portal/sessions, GET /v1/ledger, and the task list, revoke suspicious sessions, and create a replacement Key; if necessary, contact the operator to freeze the customer. An expired leaked Key can no longer pay for a renewal with its own credentials. A leaked Key that has not been revoked can still create tasks within its safety limits, so revoke it immediately once a leak is discovered. An attacker who holds only the leaked Key without an account session cannot call the emergency endpoint, but revocation propagation is affected by multi-instance caching, so you should still contact the operator as soon as possible.

New-user Trial Credit (Telegram Binding)

After registering an email/password account you can bind and verify one Telegram account under Console → Security → Bind Telegram. Once verified, the platform credits 0.10 USD of trial balance to your wallet in one go; the balance can be spent on the API without topping up.

Method and path Request Success response
GET /v1/portal/account/telegram Email/password account session enabled,status,tgUsername,verifiedAt,trialCredited,trialAmountMicros,trialMaxActive. status is empty when binding has not started, pending while waiting for confirmation inside Telegram, and verified once the credit was granted; enabled:false means the server has not enabled the bot.
POST /v1/portal/account/telegram/bind Email/password account session, X-SolveHub-Portal: 1 200 {botUsername,deepLink,expiresAt}. Open deepLink in a browser or tap it inside Telegram; the bot receives /start and the binding is verified and credited. The link is valid for 15 minutes and generating a new one invalidates the previous link.

Rules:

  • Deduplication uses the numeric Telegram user_id: one Telegram account can bind only one SolveHub account, and one account can bind only one Telegram account.
  • Binding cannot be undone: after verification, re-binding or registering again cannot claim the trial credit a second time.
  • The bot replies in the console UI language: send {"locale":"zh|en|ja"} to bind; without it the Telegram client language is used, falling back to English.
  • The trial balance never expires, but it only covers 1 concurrent task. Verification tightens max_active to 1 for that customer, so the next task can only be submitted after the previous one finishes. Any top-up (recharge code, Cregis deposit, settled order or positive adjustment) removes this system-managed limit and restores the default global concurrency; limits set manually by operations are left untouched.
  • The credit is written as one credit ledger entry (note "New-user Telegram verification reward") with a unique reference, so repeated triggers never double-credit.
  • An unbound account (status: unbound) becomes active after successful verification and can then create keys on the "API Key" page (or receive one from operations when customerKeyCreation is false).

Account and API Keys

Method and path Request/parameters Success response
GET /v1/me Authenticated Customer profile customerId,name,status,statusReason,contactEmail,contactEmailVerified,createdAt and the key view of the current credential. keyId may be empty for account sessions.
PUT /v1/me Email/password account session; {"contactEmail":"new@example.com"} The updated account profile; contactEmailVerified is false for the new email, which must be verified before it receives business notifications. Leave it empty to receive no email.
GET /v1/me/prices Authenticated (any scope; expired Keys can also read it) This customer's effective unit prices, with the same structure as GET /v1/prices, plus negotiated and listPriceMicros; see "Prices" below.
GET /v1/keys/policy Authenticated customerKeyCreation,maxActiveKeys,maxValidityDays,customerRenewal,reminderDays.
GET /v1/keys account {"keys":[APIKeyMeta...]}; the list contains no plaintext; Keys with viewable:true can be viewed via the reveal endpoint below.
POST /v1/keys Email/password account session; {"label":"prod","scopes":["tasks"],"ipAllowlist":["203.0.113.0/24"],"expiresInDays":30,"spendLimit24hMicros":100000000,"spendFreezeHourMicros":20000000} 201 {"keyId":"uuid","apiKey":"sh_...","key":APIKeyMeta}; the plaintext is stored encrypted and can be viewed again later with reveal. If the limits are omitted, they default to 100 and 20 USD respectively; empty scopes mean all scopes, and an empty allowlist means no source restriction.
GET /v1/keys/{id} The Key itself or account scope APIKeyMeta.
POST /v1/keys/{id}/reveal Account session, X-SolveHub-Portal: 1; {"password":"..."} when TOTP is not enabled, {"code":"123456"} (one-time code or recovery code) when enabled 200 {"apiKey":"sh_..."}, with Cache-Control: no-store; every view is written to the audit log. Key sessions return 403 ACCOUNT_SESSION_REQUIRED; a wrong password returns 400 INVALID_PASSWORD; a missing or wrong second-factor code returns 400 TOTP_REQUIRED / 400 TOTP_INVALID; a Key without stored plaintext (an old Key, or the server has no data encryption key configured) returns 409 KEY_NOT_VIEWABLE; an old Key can be enabled by pasting it once via bind-key; a revoked Key returns 409 KEY_REVOKED.
PUT /v1/keys/{id} Email/password account session; submit label,scopes,ipAllowlist,version as a whole; spendLimit24hMicros,spendFreezeHourMicros,clearSpendFreeze may be added The updated APIKeyMeta; customers cannot edit the expiry time directly. version prevents overwriting concurrent changes. scopes, ipAllowlist (may be empty arrays, meaning all scopes and no source restriction respectively), and the version read earlier are all required; omitting any of them or passing null returns 400 INVALID_KEY_REQUEST with no change. Setting clearSpendFreeze:true only lifts the freeze and does not clear the historical limit window.
DELETE /v1/keys/{id} Email/password account session 204; a regular revocation cannot revoke the last valid Key.
GET /v1/keys/renewal-plans Authenticated {"enabled":true,"plans":[{"code":"month","name":"30 days","days":30,"priceMicros":0}],"currency":"USD"}; the price is an example; always read the live value.
POST /v1/keys/{id}/renew A valid Key renewing itself, another valid Key with the account scope, or an account session; Idempotency-Key of 8–128 characters; {"plan":"month"} 201 on first call, 200 with Idempotent-Replayed: true on replay; the response is the renewal record. An expired Key initiating its own renewal returns 403 RENEWAL_REAUTH_REQUIRED; an account session or another valid Key with the account scope can renew it.
GET /v1/keys/{id}/renewals?limit=50 The Key itself or account {"renewals":[...]}.

The notification email and the login email are verified separately, and they do not share verification status even if the addresses are the same. An unverified notification email does not receive announcements, support ticket replies, Key reminders, or financial emails; business emails queued but not yet delivered to the old address are discarded after the address changes. Emails already sent cannot be recalled. Key-only customers must first bind an email and password themselves on the "Security" page using a Key with the account scope before they can set and verify a notification email.

APIKeyMeta contains keyId,label,hint,status,scopes,ipAllowlist,expiresAt,lastUsedAt,lastUsedIp,createdBy,createdAt,revokedAt,version,spendLimit24hMicros,spendFreezeHourMicros,spendFrozenAt,viewable; viewable means the server has stored the Key's encrypted plaintext, which can be viewed in an account session after second-factor verification; the plaintext is deleted as soon as the Key is revoked. expiresAt:null means the Key does not expire; the status is active, expired, or revoked. A non-empty spendFrozenAt means task creation is frozen because the cumulative holds within one hour exceeded the threshold. Both limits are positive integers in micro-USD (micros), up to 10,000 USD, and the one-hour threshold cannot exceed the 24-hour limit. Limits are counted conservatively in five-minute windows, which may add up to about five minutes of extra restriction; refunds after failure or cancellation do not release safety allowance, to prevent probing by repeatedly creating tasks and getting refunds. The request that triggers the freeze creates no task and does not touch the wallet; if a contact email is configured and the email service is available, a notification is sent, but the email must not be treated as a precondition for the freeze taking effect. Review tasks and the ledger before lifting a freeze. Labels are 1–100 characters; the IP allowlist accepts single addresses or CIDRs, up to 32 entries. Do not put API Keys in URLs, browser local storage, or support ticket bodies. A Key's account scope can read other Keys' metadata and manage Webhooks, but cannot issue or modify Keys; it is recommended to grant task processes only tasks.

Prices, Tasks, and Usage

GET /v1/prices requires no login and returns an object keyed by type; each entry contains available, plus the global priceMicros,currency when enabled or reason when disabled, and may also include version,updatedAt. available means the type is enabled in the price configuration and does not guarantee real-time processing capacity.

To learn your actual unit price, call the authenticated endpoint GET /v1/me/prices: its structure is the same as GET /v1/prices, and enabled types additionally include listPriceMicros (the public list price) and negotiated. When negotiated:true, priceMicros is your negotiated price; if the operator raises the procurement cost so that the negotiated price no longer applies, it automatically falls back to the public list price with negotiated:false. Disabled or unconfigured types are not quoted, even if a negotiated price exists. Quoting and task creation use the same rules, but prices may still change between the two requests; the final hold amount is the priceMicros in the creation response. version is the public price list version; changes to negotiated prices do not change it. POST /v1/tasks requires the tasks scope and an Idempotency-Key of 8–128 characters. Request example:

POST /v1/tasks HTTP/1.1
Authorization: Bearer sh_EXAMPLE
Content-Type: application/json
Idempotency-Key: order-12345-turnstile

{"type":"turnstile","task":{"websiteURL":"https://example.com","websiteKey":"SITE_KEY"}}

On success, a 202 task object is returned, such as {"taskId":"uuid","type":"turnstile","status":"queued","priceMicros":200,"traceId":"uuid","createdAt":"2026-10-02T00:00:00Z"}. The task object may also include solution, errorCode, and finishedAt. On failure, errorCode is TASK_FAILED or TASK_TIMEOUT; when uncertain (pending verification), it is TASK_UNCERTAIN. Results contain only the fields the customer needs to complete their business; for diagnostic information, contact support with the taskId and X-Request-ID. Task statuses: queued (cancellable), dispatching, processing, uncertain, completed, failed, cancelled. uncertain is not a terminal state: the platform temporarily cannot confirm the task's processing result or cost, and the amount remains on hold; keep querying the original task and provide the taskId to support, and do not create a replacement task with a different idempotency key. On completion, the frozen sale price is charged; on an explicit failure/cancellation, the hold is refunded.

A failed task may also include failureReason: a sanitized, length-limited explanation of an explicit upstream failure, for example IP Blocked (fake page). If no description was supplied, a stable failure code may be used instead. Use errorCode and status for control flow; treat failureReason as diagnostic text rather than an enum.

Method and path Parameters Response
GET /v1/tasks/{id} tasks 200 task object.
GET /v1/tasks status,type,from,to,limit,cursor,offset; times in RFC 3339, limit 1–100, default 50; offset 0–100000, cannot be used together with cursor {"tasks":[...],"nextCursor":"..."}; pass a non-empty cursor unchanged to fetch the next page. With offset, pagination is by page number, and the total count total matching the filters is also returned.
GET /v1/tasks/{id}/events tasks {"events":[{"traceId":"...","from":"queued","to":"processing","message":"...","at":"..."}]}.
POST /v1/tasks/{id}/cancel tasks 200 task object; only queued tasks can be cancelled, otherwise 409 TASK_NOT_CANCELLABLE.
GET /v1/usage tasks; optional from,to (up to 90 days), granularity=day&tz=Asia/Shanghai {"from":"...","to":"...","items":[{"type":"turnstile","created":1,"completed":1,"failed":0,"cancelled":0,"uncertain":0,"chargedMicros":200,"reservedMicros":0}],"currency":"USD"}. With daily granularity, each item has date.

When the same customer reuses the same key with an identical request body within the task idempotency record retention period, the original task is returned without placing another hold; the same key with a different request returns 409 IDEMPOTENCY_CONFLICT. On a client network timeout or 5xx, first retry with the same key, record the returned taskId, and keep querying. The server periodically purges task idempotency records older than about 25 hours, so clients should store taskId long-term and must not rely on old idempotency keys for deduplication after purging. The task task must not exceed 7 MiB.

Task Types and Parameters

The outer type is required; the inner task.type may be omitted, but if provided it must match the outer one. All types reject unlisted fields; clientKey or any external service key must not be passed. URLs in the table below must be complete HTTP(S) URLs; the proxy port is a string, and the proxy protocol can only be http.

type Required task fields Optional fields and limits
ocr base64: image Base64, may include a data:image/...;base64, prefix; string up to 5,242,880 bytes None; the image must be decodable.
turnstile websiteURL,websiteKey useragent; cdata,action; proxy quadruple proxyType,proxyAddress,proxyPort with optional login credentials.
interstitial websiteURL,proxyType,proxyAddress,proxyPort siteKey (the page's Turnstile sitekey, enables a fast path), useragent,source (boolean), method (GET/POST), proxy login credentials.
perimeterx websiteURL,websiteKey and the proxy quadruple useragent, proxy login credentials.
datadome websiteURL,captchaUrl and the proxy quadruple useragent; Proxy login credentials.
akamai websiteURL and the proxy quadruple useragent; ApiURL, proxy login credentials.
aliyun websiteURL,websiteKey,prefix and the proxy quadruple region (sgp/cn), useragent, proxy login credentials.
imperva websiteURL; when isUtmvc=true, scriptURL is also required, otherwise reeseScriptUrl is required cookies (up to 100 {name,value}), language,useragent, optional proxy.
awswaf websiteURL,WafScriptUrl Optional proxy.
alix5sec websiteURL useragent, optional proxy; disabled by default in the catalog; live prices/enabled status are authoritative.
funcaptcha proxy (a full http/https/socks5 address, port required), challengeInfo, browserInfo challengeInfo must contain at least the strings publicKey,site,surl,capiMode,styleTheme,locationHref,documentReferrer,treeStructure, the arrays ancestorOrigins,treeIndex, and the booleans languageEnabled,jsfEnabled; browserInfo must contain at least the User-Agent string and the booleans Linux,Mac,Mobile,Safari. Optional fields, when sent, must use the types in the FunCaptcha guide.
funcaptcha_classification base64Image (decodable Base64), gameType (non-negative integer), variant Whether it is enabled is determined by live prices; currently disabled by default.
perimeterx_cookies appId (PerimeterX application ID, ≤ 64 bytes), site (target page URL), proxy (a full http/https/socks5 address, port required) No other fields; the result is solution.cookies. Whether it is enabled is determined by live prices; currently disabled by default.

websiteURL is limited to 2048 bytes; websiteKey 256, proxyAddress 256, proxyPort 5, and the proxy username/password 256 each. captchaUrl and WafScriptUrl are limited to 4096; server-side validation is authoritative for the lengths of specific optional fields. The complex structure of funcaptcha must be submitted according to that type's validation rules; avoid mixing in fields from other CAPTCHA types.

Allowed fields for funcaptcha.challengeInfo: publicKey,site,surl,apiUrl,capiMode,styleTheme,locationHref,documentReferrer,documentTitle,iframeUrl,ancestorOrigins,treeIndex,treeStructure,languageEnabled,jsfEnabled,appMode,uniqueFingerprint,oldFingerprint,overrideFingerprint,syntheticFingerprint,randomProtoChain,randomVideoExtended,staticSpeechData,customTimezoneOffset,historyLength,noIframe,lowBandwidthMode,storageAccess,solvePOW,cookieTimestamp,disableArid,disableAridCookie,disableCookies,disableCookiesAnalytics,disableGT2Cookies,disableNavigatorCookies,powPause,exploitMode,skipHighWaves,submitAnswerPause,extraData. Allowed fields for browserInfo: Accept-Language,Cache,Cookie,DNT,HTTP3,Linux,Mac,Mobile,Safari,Sec-Ch-Ua,Sec-Ch-Ua-Platform,User-Agent,dataBrands,languages,randomTLS,userbrowser. Unlisted fields are rejected.

Balance and Top-ups

Method and path Request Response
GET /v1/balance billing {"availableMicros":1000000,"reservedMicros":0,"currency":"USD"}.
GET /v1/ledger?before=123 billing; before is the positive integer nextCursor from the previous page {"entries":[{"id":123,"kind":"credit","availableDeltaMicros":1000000,"reservedDeltaMicros":0,"at":"..."}],"nextCursor":...}.
POST /v1/recharge-codes/redeem billing; {"code":"..."} 200 {"amountMicros":1000000}; recharge Keys are single-use; used/expired/revoked Keys uniformly return 409 RECHARGE_CODE_UNAVAILABLE.
GET /v1/topup-orders / GET /v1/topup-orders/{id} billing; historical records {"orders":[...]} / order object.
PUT /v1/topup-orders/{id} / POST /v1/topup-orders/{id}/cancel billing; only handles existing historical orders that can still be modified/cancelled. A PUT request may contain paymentChannel,payerReference,customerNote, at least one field The updated order object.

Top-ups are supported only via manual crediting by an administrator or via recharge Keys issued by an administrator and redeemed by the customer. The customer POST /v1/topup-orders always returns 410 TOPUP_ORDERS_DISABLED, and GET /v1/topup-orders/instructions returns 410 PAYMENT_METHODS_DISABLED. The platform has no public bank card, Alipay, WeChat, USDT, PayPal, or automatic payment API. The ledger and GET /v1/balance are authoritative for the balance; it is never increased based on client-side payment prompts.

Support Tickets and Announcements

Method and path Request/parameters Response
GET /v1/tickets?status=&limit=50&offset=0 Authenticated; statuses awaiting_support, awaiting_customer, closed {"tickets":[Ticket...],"total":1}.
POST /v1/tickets Optional Idempotency-Key; {"category":"technical","subject":"Unexpected task result","body":"Please help check task ..."} 201 {"ticket":Ticket}; an idempotent replay returns 200 with Idempotent-Replayed: true. Categories are technical,billing,account,other; subject 5–120 characters, body 10–5000 characters.
GET /v1/tickets/{id}?before=123 Authenticated; up to 50 messages per page {"ticket":Ticket,"messages":[TicketMessage...],"nextBefore":123}; a non-zero cursor continues paging. Internal notes are not shown to customers.
POST /v1/tickets/{id}/messages Optional Idempotency-Key; {"body":"Additional information"}, body 1–5000 characters 200 {"ticket":Ticket}; an idempotent replay carries Idempotent-Replayed: true and does not append a message; replying to a closed support ticket may return 409 TICKET_CLOSED.
POST /v1/tickets/{id}/close / POST /v1/tickets/{id}/reopen Authenticated 200 {"ticket":Ticket}.
GET /v1/announcements?limit=50&offset=0 No login required {"announcements":[{"id":"uuid","title":"...","body":"...","publishedAt":"..."}],"total":1}; only published website announcements are shown. Email-only announcements are not displayed here.

Ticket contains id,customerId,customerName,category,subject,status,priority,assignedTo,version,createdAt,updatedAt,lastMessageAt,closedAt,messageCount; TicketMessage contains id,ticketId,authorKind,authorRef,body,internal,createdAt. Support tickets have a limit on open tickets (20) and a rate limit (10 created within 24 hours).

Creating a support ticket and customer replies may carry an Idempotency-Key (8–128 visible ASCII characters, no spaces; an invalid format returns 400 INVALID_IDEMPOTENCY_KEY). Keys are isolated per customer and valid for 24 hours: retrying the same request with the same key (leading/trailing whitespace does not matter) returns the current state of the original support ticket without creating a duplicate or appending a message, even if the ticket has been closed or the rate limit has been reached; using the same key for different content, a different support ticket, or across creation and reply returns 409 IDEMPOTENCY_CONFLICT. On network errors or 5xx, retry unchanged with the same key; each new support ticket or reply must use a new key. Without this header, the original behavior is kept and every request is written. Announcement bodies are displayed as plain text; email announcements are sent asynchronously, and the website endpoint does not indicate that the email has been delivered.

Webhook

The multi-endpoint interface is recommended, with up to 10 endpoints; the address must be a publicly accessible HTTPS 443 URL, with no credentials, query string, or fragment. The signingSecret from creation and secret rotation is returned only once.

Method and path Request/parameters Response
GET /v1/webhooks account {"endpoints":[...],"availableEvents":[...],"maxEndpoints":10}.
POST /v1/webhooks {"url":"https://hooks.example.com/solvehub","description":"prod","events":["task.completed","task.failed"],"enabled":true} 201 {"endpoint":WebhookEndpoint,"signingSecret":"whsec_..."}; empty events subscribes to all task events by default.
GET /v1/webhooks/{id} / PUT /v1/webhooks/{id} / DELETE /v1/webhooks/{id} account; PUT is a full update of url,description,events,enabled Endpoint object / updated endpoint / 204.
POST /v1/webhooks/{id}/rotate-secret account; optional {"graceHours":24} 200 {"endpoint":...,"signingSecret":"..."}; during the grace period both old and new signatures may be sent.
POST /v1/webhooks/{id}/test account 202 {"eventId":123,"status":"pending"}. Each customer can create at most one test delivery every 60 seconds across all endpoints; going too fast returns 429 WEBHOOK_TEST_RATE_LIMITED with Retry-After: 60.
GET /v1/webhook/deliveries account; optional endpointId,status,before,limit; statuses pending,delivered,dead {"deliveries":[...],"nextCursor":"..."}.
POST /v1/webhook/deliveries/{id}/retry account; for failed deliveries 202 {"status":"pending"}.
GET /v1/webhook / PUT /v1/webhook / DELETE /v1/webhook account; legacy primary endpoint interface, PUT {"url":"https://..."} Legacy single-endpoint view / configuration with signingSecret / 204.

Subscribable events: task.completed, task.failed, task.cancelled, task.uncertain, api_key.expiring, api_key.expired, api_key.renewed, balance.low, topup_order.paid, topup_order.rejected, topup_order.expired. The manual test event is webhook.test and requires no subscription. Some historical order events apply only to existing orders.

balance.low is generated when the available balance drops from at least 5 USD to below 5 USD due to a task hold, Key renewal, manual deduction, or reversal; the held balance does not count toward the available balance. The event payload contains availableMicros and thresholdMicros. A verified notification email also receives an email; an unverified email does not. If the balance is later topped back up to at least 5 USD, a new alert is generated when it drops below again. Historical balances already below the threshold do not trigger a backfilled alert when this feature is deployed.

The platform POSTs JSON to the endpoint with the headers X-SolveHub-Event-ID (decimal event ID), X-SolveHub-Event-Type, X-SolveHub-Timestamp (Unix seconds), and X-SolveHub-Signature. Example task event body: {"event":"task.completed","occurredAt":"2026-10-02T00:00:00Z","taskId":"uuid","type":"turnstile","status":"completed","traceId":"uuid","priceMicros":200}; test events contain event,testId,endpointId,occurredAt. The signature is sha256= followed by the hexadecimal value of HMAC-SHA256(signingSecret, timestamp + "." + raw request body bytes); during a rotation grace period the signature header may contain two comma-separated sha256= values, and verifying either one is sufficient. After reading the raw bytes, the receiver should check the time window, compare signatures in constant time, deduplicate by event ID, and then return 2xx quickly; non-2xx responses are retried, and deliveries may be duplicated or arrive late. The event payload is not the final basis for settlement; after receiving a task event, query GET /v1/tasks/{id}.

Compatibility Endpoints

POST /createTask, POST /getTaskResult, and POST /getBalance accept a clientKey JSON field; both business success and failure may return HTTP 200, so you must check errorId (0 success, 1 failure) and errorCode. createTask supports native types and the aliases ImageToTextTask, TurnstileTask, TurnstileTaskProxyless, TurnstileTaskProxyLess, AntiTurnstileTaskProxyLess.

Endpoint Request example Success response and notes
POST /createTask {"clientKey":"sh_...","task":{"type":"TurnstileTaskProxyless","websiteURL":"https://example.com","websiteKey":"SITE_KEY"}} {"errorId":0,"taskId":"uuid"}; compatible image OCR requests use ImageToTextTask and the body image field.
POST /getTaskResult {"clientKey":"sh_...","taskId":"uuid"} In progress contains {"errorId":0,"status":"processing"}, including native uncertain; completed contains status:"ready",solution,cost,createTime,endTime,solveCount. cost is a USD string, and times are Unix seconds. Failure/cancellation returns errorId:1,errorCode:"ERROR_CAPTCHA_UNSOLVABLE".
POST /getBalance {"clientKey":"sh_..."} {"errorId":0,"balance":1.25}, a floating-point number in USD; for financial reconciliation, use /v1/balance with integer micro-USD (micros).

Compatibility errors may be ERROR_KEY_DOES_NOT_EXIST, ERROR_KEY_DENIED_ACCESS, ERROR_IP_NOT_ALLOWED, ERROR_ACCOUNT_SUSPENDED, ERROR_BAD_PARAMETERS, ERROR_TASK_NOT_SUPPORTED, ERROR_NO_SLOT_AVAILABLE, ERROR_ZERO_BALANCE, ERROR_NO_SUCH_CAPCHA_ID, or ERROR_SERVICE_UNAVAILABLE. The compatibility creation endpoint has no idempotency key, so blindly retrying after a timeout may create another task and place another hold on the balance; new integrations should use /v1/tasks.

Minimal Integration Example

export SOLVEHUB_URL='https://www.tiam.codes'
export SOLVEHUB_API_KEY='sh_REPLACE_ME'
curl -sS -H "Authorization: Bearer $SOLVEHUB_API_KEY" "$SOLVEHUB_URL/v1/balance"
curl -sS -X POST "$SOLVEHUB_URL/v1/tasks" \
  -H "Authorization: Bearer $SOLVEHUB_API_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: your-order-12345' \
  -d '{"type":"turnstile","task":{"websiteURL":"https://example.com","websiteKey":"SITE_KEY"}}'

When integrating, save the taskId, idempotency key, and X-Request-ID. Do not submit full API Keys, recharge Keys, passwords, TOTP secrets, or raw proxy credentials in logs, screenshots, or support tickets.

DataDome challenge URL helper

POST /v1/datadome/build-url requires a valid Key with tasks scope and an active account. It returns synchronously, is free and creates no solving task. Send websiteURL (up to 2048 bytes) and at least one of body, location, or captchaUrl. body is captured HTML/JSON (up to 1 MiB); each URL field accepts up to 4096 bytes. A supplied captchaUrl takes precedence. Never provide credentials for an internal service.

curl -sS -X POST 'https://www.tiam.codes/v1/datadome/build-url' \
  -H 'Authorization: Bearer sh_REPLACE_ME' \
  -H 'Content-Type: application/json' \
  --data '{"websiteURL":"https://example.com/login","body":"{\"url\":\"https://geo.captcha-delivery.com/captcha/?cid=EXAMPLE\"}"}'
{"captchaUrl":"https://geo.captcha-delivery.com/captcha/?cid=EXAMPLE","challengeType":"captcha"}

Success returns captchaUrl,challengeType; pass the URL unchanged to a DataDome solving task. Errors: 400 INVALID_DATADOME_PAYLOAD, 422 DATADOME_CHALLENGE_NOT_FOUND, 503 DATADOME_BUILDER_UNAVAILABLE (retry with backoff), or 502 DATADOME_BUILDER_FAILED. Compatibility POST /buildDataDomeUrl takes a SolveHub clientKey in JSON and uses HTTP 200 with errorId/errorCode/errorDescription. The helper needs no Idempotency-Key; solving tasks retain their existing idempotency and billing rules.

See Supported User Agents for browser coverage and copyable UA examples. Processing soft windows start after task creation is confirmed: 240 seconds for DataDome and 210 seconds for PerimeterX.

The optional keyId query parameter on GET /v1/tasks and GET /v1/usage filters tasks for that key within the authenticated customer. Omitting it preserves aggregate usage across all keys. A task response may include keyId, an identifier rather than the secret; older tasks may omit it. GET /v1/ledger supports kind (credit,reserve,capture,refund,fee,reversal,adjustment) across the full history; retain the same kind when paging.

Related documentation

Start with cURLCreate account

Last updated: