Changelog
Release history for the ByteKit SDKs, CLI, and MCP server
For AI agents: https://bytekit.com/llms.txt — every docs page is available as markdown by appending .md
Each section below is the CHANGELOG.md kept alongside that client’s source in the ByteKit
repository. Entries record what a release changed, including names and options it retired.
@hunt-labs/bytekit-sdk
TypeScript SDK, published on npm.
[Unreleased]
[0.11.9] - 2026-10-02
Changed
- Terms-version doc comments describe the new onboarding contract (#5055). Bootstrap no
longer records Terms acceptance: the internal
POST /v1/account/bootstrapterms_versionbody is now documented as a version guard, with acceptance recorded byPOST /v1/account/accept-terms. The accountterms_versiondescription now says an empty string means the account has not accepted the Terms yet. Regeneratedsrc/generated/api.d.ts; description text only, no type or SDK method signature moves.
[0.11.8] - 2026-09-30
Changed
- Bulk job types add
total_billed_bytesand deprecate the bulk credit fields (#5021). Every bulk job body (POST/GET/DELETE /v1/bulk,/v1/scrape/bulk,/v1/fetch/bulk, and theGET /v1/bulklist) gainstotal_billed_bytes: number | null: the bandwidth billed for the job, in bytes, after multipliers. It isnullon the create response and for a bulk that predates the field.total_credits_chargedand the bulk itemcredits_chargedare marked@deprecated; they report0under thedualandbytesbilling modes. Regeneratedsrc/generated/api.d.ts; no SDK method signature moves.
[0.11.7] - 2026-09-29
Changed
- Internal
GET /v1/billing/topup-balancetypes addbandwidth.debt_bytes(#4960, #4975). The bytes the account still owes for bandwidth a finished request used beyond everything it could pay for, net of any repaid. Declared on that endpoint only, not on the sharedBandwidthBalanceschemaGET /v1/usagereturns. No SDK method calls it.
[0.11.6] - 2026-09-24
Changed
- Regenerated
src/generated/api.d.ts(#4865): a documentation-wording correction to thePOST /v1/schemabilling description, which now says the page bandwidth is billed at its upstream wire bytes with no endpoint factor instead of "exactly like/v1/scrape". No behavior change: doc comments only, no SDK signature moves.
[0.11.5] - 2026-09-22
Changed
- Internal
GET /v1/logstypes list schema rows (#2373). Bothendpointenums addschema, for/v1/schemastructured-extraction requests, whose row ids carry the newsch_prefix. No SDK method calls it.
[0.11.4] - 2026-09-21
Changed
- Regenerated
src/generated/api.d.ts(#4732): the422responses ofGET /v1/fetchandPOST /v1/fetchdeclare the optionalX-Fetch-IDheader, present when the failed request was recorded and naming its row in Logs (sc_<hex>for the header'sft_<hex>). Doc comments only; no SDK signature moves.
[0.11.3] - 2026-09-21
Changed
- Regenerated
src/generated/api.d.ts(#4669):GET /v1/fetchandPOST /v1/fetchdocument a second422condition. A fetch whose retries were exhausted against the target used to answer503with codeinternal_errorand the message "Fetch service unavailable.", which read as an outage of the API; it now answers422with codeblockedwhen the target's bot protection refused every attempt andupstream_error(message "Failed to fetch URL.") otherwise, and it is still charged nothing. A client that matched that503must match422and the code instead; the503internal_errorremains only for a deployment whose fetch service endpoint or internal token is not configured. Doc comments only; no SDK signature moves.
[0.11.2] - 2026-09-18
Changed
- Regenerated
src/generated/api.d.ts(#4582): bulk itembillingisBillingNode | null. - Regenerated
src/generated/api.d.ts(#4584):POST /v1/schemanow rejects slow-path rendering options (wait_until=networkidle,delay_ms,wait_for_selector, cookies, custom headers) with error codeunsupported_optioninstead ofunsupported_url, still HTTP 400.unsupported_urlis removed from the API and has no other emitter; a client matching on it must matchunsupported_optionfor this case. Doc comments only; no SDK signature moves.
[0.11.1] - 2026-09-17
Changed
- Regenerated
src/generated/api.d.ts(#4563): the internallistBulkoperation'sstatusquery parameter and list-itemstatusnow admitcancelled. Additive; no SDK method callslistBulk, so no published signature moves.
[0.11.0] - 2026-09-14
Added
solve_challengeonscrape.create(#4447).ScrapeOptsnow carriessolve_challenge?: boolean, and the generatedScrapeRequestschema type carries the field with its server defaultfalse. Whentrue, the scrape attempts to clear a Cloudflare challenge; success is not guaranteed. It is billed as a normal browser render. A scrape that omits the option sends nosolve_challengekey, so existing calls are unchanged on the wire.credits_scrapeon the usage response types (#4452). The generatedGET /v1/usageresponse type and eachGET /v1/usage/dailydata item now carrycredits_scrape: number: credits drawn under the scrape usage source, such as/v1/schemaextractions. The gateway always charged this bucket but never returned it, so the published per-source credit fields could not add up tocredits_used.
Changed
- Internal
GET /v1/bulkis cursor-paginated and its 100-job cap is gone (#4508). The generatedlistBulktypes record a changed response shape: the operation used to return a bare array of at most the 100 newest jobs, and now returns the same page envelopeGET /v1/monitorsdoes,{ data, next_cursor, has_more }, pluslimitandcursorquery parameters (limitis 1 to 100, default 25). Astatusfilter and acursorcombine in one request:statusnarrows the set and the cursor addresses a position inside it. The operation's summary also no longer says "in-flight", which was wrong — it has always returned jobs of every status,cancelledincluded. This is a breaking change to the wire shape of anx-sdk-scope: internaloperation; no SDK method calls it, so no published method signature moves. GET /v1/logsbilling_multiplieris the bandwidth factor, not a credit count (#4469). The generated logs-row type records the changed meaning of a public field: the value is nowbillable_bytes/raw_bytesrounded to 2 decimal places — the factor the request settled at, carrying the endpoint, cache-hit and clean-markdown factors, and the flat 1.5 Phase-1 factor on screenshots and recordings. It used to be the row's persistedcredits_charged, an account credit count charged per capture option, so two screenshots billed at the same 1.5 factor reported2and1. The 2 decimal places are load-bearing rather than cosmetic:billable_bytesis itself rounded, so an odd byte basis would otherwise surface as1.500001943158721instead of1.5on roughly half of all such rows. Every factor the field can report is a multiple of 0.25, so 2 places is exact for any basis of 100 bytes or more, andraw_bytes×billing_multiplierrounded to the nearest byte still reproducesbillable_bytes. Thebillable_bytesdoc comment therefore no longer says it is NOTraw_bytes×billing_multiplier. The field is also null on more rows than before: a search or webhook row has no byte basis, a capture whose Phase 1 was skipped has a zero one, and a capture old enough to predate the Phase-1 basis being recorded has none at all. One residual is documented rather than fixed: a markdown/v1/fetchrow completed before the billing decomposition was recorded reports 1.0 where settlement charged 1.5, because nothing on such a row distinguishes it. The type shapes are unchanged (number | null), and no SDK method calls this endpoint.- An all-null
/v1/schemaextraction your schema allows is validated again (#4490). The generated/v1/schema200 response type records the exception to #4455's rule: an output whose every leaf isnullstill arrivesvalidated: truewith the object intact, and is charged, when the schema you sent declares"null"in thetypeunion of every leaf the output returned asnull. Only that form counts —nullable: true, anenumcontainingnullandconst: nulldo not, and a response that is itselfnullrather than an object is never accepted however the root is declared. Everything else stays the #4455 miss. Thedatadoc comment now also states the other half: avalidated: trueresponse always carries an object, nevernull. The type shapes are unchanged; only their doc comments moved. - An all-null
/v1/schemaextraction is reported, not validated (#4455). The generated/v1/schema200 response type documents a new additive warning code,empty_extraction: a schema-valid output whose every leaf isnullnow arrives withdata: nullandvalidated: false, and is not charged the per-request credits. Previously it arrived asvalidated: truewith the all-null object and no warning. Thedataandvalidateddoc comments now also say that everyvalidated: falseresponse carriesdata: null; the old comment claimed exhausted retries returned the last raw output, which the API never did. The type shapes are unchanged; only their doc comments moved. - An inline
POST /v1/scrapefailure is documented with a persistedid(#4454). The gateway now records a sync scrape that endsupstream_errororblockedas a failed scrape job, so itsfailedenvelope carries that job'ssc_id instead ofnull,scrape.get(id)returns the same envelope, and the request appears in the dashboard Logs. The generatedcreateScrape200 andScrapeErrorEnvelopedoc comments and thescrape.createdoc comment no longer sayid: null. The type shapes are unchanged (idstaysstring | null), andscrape.createstill throwsByteKitErroron that envelope exactly as before. - Internal
GET /v1/logstypes list search and webhook rows (#4460). Bothendpointenums addsearchandwebhook; a row'ssourceis nowstring | null. No SDK method calls it.
[0.10.0] - 2026-09-11
Changed
- A terminal
POST /v1/scrapefailure now throws instead of resolving (#1870). The gateway used to report an upstream fetch failure — unreachable host, refused connection, or bot protection that defeated every proxy tier — as HTTP 503; it now answers HTTP 200 with the canonicalfailedenvelope (id: null), because a definitive upstream outcome is a result, not a transport error.!response.okwas this SDK's only failure seam, so without this change that body would resolve like a success and silence every caller'scatch.scrape.create(and anyrequest/requestWithResponsecall toPOST /v1/scrape) now raisesByteKitErrorcarrying the envelope'scodeandmessage, the actual status200, the response headers, anderror.detailswhen one is supplied. A legacy 503 from a not-yet-upgraded gateway still throws through the unchanged non-2xx decoder, so the SDK works against both. Upgrade note: if you inspectedresult.status === 'failed'afterscrape.create, move that handling into acatch. - Everything else is untouched:
scrape.get(id), bulk item results, and every other endpoint still deliverfailedas data;requestRaw(/v1/fetch) never parses JSON and is unaffected; 204 and empty-body handling are unchanged.request()now delegates torequestWithResponse()so the guard has a single application site — identical in every observable respect but the return value, as both were already documented.
[0.9.4] - 2026-09-06 [NEVER PUBLISHED]
This version was cut but NEVER PUBLISHED — npm latest goes from 0.9.3 straight to 0.10.0.
Publication is triggered by CI on main from the version the manifest declares, and 0.10.0
(#1870) superseded 0.9.4 in the tree before any release reached main. Everything below
reaches consumers in the 0.10.0 artifact, which contains all of it; nothing is installable
at 0.9.4: npm i @hunt-labs/[email protected] fails.
Added
- Bulk fetch, scrape, and mixed-result reads now accept optional cursor, limit, and status
page parameters while preserving the existing
(id, requestOptions?)call shape (#3717). Generated response types publish stable item identities, page cursors and completeness, whole-job counters, exact mixed artifact keys, artifact warnings, bounded webhook recovery metadata, and theartifact_unavailableerror contract.
Fixed
- Corrected the artifact-URL fallback documentation on
GET /v1/bulk/{id}/screenshots(image_url,content_url,markdown_url) andGET /v1/logs(artifact_url) (#3717). Those fields were documented as null-plus-artifact_unavailablewhen no signature can be minted; the servers have always returned the row's stored value unchanged instead —nullwhile nothing is stored, and otherwise the private unsigned storage URL, which answers401— and raise no warning.GET /v1/fetch/bulk/{id}does null the field and reportartifact_unavailable, and its documentation is unchanged. No server behaviour changed and no type moved; the descriptions now match what the endpoints return.
Changed
- Regenerated
src/generated/api.d.tsagainst the correctedtopup_balance_bytesdescription onGET /v1/usage(#4251). The description read "Remaining purchased top-up bytes", which contradicted its own "Equal totopup_bytes" clause: the value covers purchased top-up lots and the one-time starter grant, which is what the endpoint has always returned. The clause recording that the figure is already net of consumed overage, and the "Present only in dual/bytes billing mode" clause, are both unchanged and still true. Types are unchanged and no server value changed; only the documentation on them moved. - Regenerated
src/generated/api.d.tsagainst the clarifiedcache_age_sdescription onScrapeSuccessEnvelope(#4237). The description now records thatGET /v1/scrape/{id}reportscache: hitwithoutcache_age_s: the age is a property of the replay that happened at completion time, is not persisted on the row, and cannot be derived honestly afterwards. The one-directional "Present only oncache: hit" clause is unchanged and still true. Types are unchanged; only the documentation on them moved. - Regenerated
src/generated/api.d.tsagainst the completedcachepresence list onScrapeSuccessEnvelope(#4203). The field's description now namesGET /v1/scrape/{id}as a third delivery path, alongside the synchronous response and webhook delivery: a polled scrape now reports the cache disposition its row recorded, and omits the field — rather than defaulting it tomiss— for a row that recorded none. Types are unchanged; only the documentation on them moved. - Regenerated
src/generated/api.d.tsagainst the correctedPUT /v1/billing/auto-topuprequest body (#3984).monthly_cap_centsis now typednumber | null: an explicitnullis the wire representation of "no monthly cap", and it is what the endpoint has always accepted — the request schema simply never documented it, while the 200 response schema already did. Omitting the key whileenabledistrueis still rejected with422, so sendnullrather than dropping the field. No server behaviour changes; the type widens to match what the API already accepted. - Regenerated
src/generated/api.d.tsagainst the corrected monitorscrape_optionsdocumentation (#4024). The description no longer listsheadersamong the accepted fields —POST /v1/monitorsnever honoured a caller-suppliedheadersoption and now rejects it with the same 400 every other capture endpoint returns for the field. Types are unchanged; only the documentation on them moved. - Regenerated
src/generated/api.d.tsagainst the completedX-Scrape-Proxy-Tier-Labelomission list (#4030). The header's description now names a third case in which no tier is reported: a response replayed from stored bytes after this request's own cache lookup missed, which arrives asX-Scrape-Cache: missand likewise ran no proxy tier. The two cases already documented — a cache hit, and responses the fast path did not serve — are unchanged. Types are unchanged; only the documentation on them moved. - Regenerated
src/generated/api.d.tsagainst the qualifiedX-Scrape-Proxy-Tier-Labelomission list (#4132). The third case that list records — a response replayed from stored bytes after this request's own cache lookup missed — is not reachable through this API: every caller of the internal fetch path closes the handler branch that produces it, and the branch survives only as a deploy-skew safeguard. The description now says so rather than presenting the case as ordinary behaviour. The two reachable cases — a cache hit, and responses the fast path did not serve — are unchanged. Types are unchanged; only the documentation on them moved.
[0.9.3] - 2026-09-04
Changed
- Regenerated
src/generated/api.d.tsagainst the corrected queued-scrapestatus_urldocumentation (#3925).status_urlis a root-relative path, never an absolute URI: join it onto the API base URL you called (https://api.bytekit.com, no trailing slash) to get the URL to poll. The spec had declared itformat: uriwith an absolute example, which no server has ever emitted; the emitted value is unchanged, so no client behaviour moves. - Regenerated
src/generated/api.d.tsagainst the documented proxy-tier response headers (#3926).X-Scrape-Proxy-Tier-Labelnow documents that it reports the tier that ACTUALLY served a scrape — after any escalation, never the tier first attempted — that it is diagnostic only, and that it is omitted on a cache hit, which replayed stored bytes and ran no tier. The deprecated numericX-Scrape-Proxy-Tierdocuments the legacy1datacenter /2residential /3mobile vocabulary and its omission for the lean-browser tier. Types are unchanged; only the documentation on them moved.
[0.9.2] - 2026-09-03
Changed
-
Breaking (API): the scrape
formatsvocabulary is nowraw | markdown | links | images(#3775). The unprocessed-source format is calledraw— it has returned JSON, XML and plain text alongside HTML since #1853, so the old raw-HTML name misdescribed it — and the cleaned-article-HTML format is gone. The server accepts no alias for either retired value, so a request naming one now comes back422with the new set in the message. Updateformatsand read the content back fromformats.raw:const result = await client.scrape.create({ url, formats: ['raw', 'markdown'] }); console.log(result.formats.raw); -
Regenerated
src/generated/api.d.tsagainst the documented402contract (#3784). Every quota-enforced operation now declares402through one sharedPaymentRequiredresponse, and theX-Quota-*headers are referenced by the operations that actually emit them. Types only — no runtime behavior changed. -
The generated
eventsfield description on the scrape request type now documents the widened server default (queued,completed,failed) and that a caller may send an explicit subset to receive fewer deliveries (#3777). Types/docs only — this regeneration followed a server-side default change; the SDK carried no logic tied to the old default.
[0.9.1] - 2026-08-25
Fixed
-
The generated
pathstype for the200ofGET /v1/scrape/bulk/{id}now declaresitems(#3632). The route builds one body and varies only the status code, so the terminal200has always carried the per-URL scrape envelopes — but only the202documented them, soscrape.bulk.get()typed the results away at exactly the poll that has them. Types only: the field was already on the wire, and server behavior is unchanged. -
The generated
pathstypes now describe the202thatGET /v1/scrape/bulk/{id},GET /v1/fetch/bulk/{id}andGET /v1/bulk/{id}already serve while a bulk job is non-terminal (#3533). Each of the three returns 202 with the job envelope until the job is terminal and 200 afterwards, but only the 200 was declared, so anything typed off the spec had no shape for the response the very first poll receives. Types only:scrape.bulk.get(),fetch.bulk.get()andbulk.get()already resolved the body on any 2xx, and server behavior is unchanged.
Changed
- The generated
topup_balance_bytesdescription onGET /v1/usagenow states that the value is the remaining purchased top-up bytes, already net of any overage consumed against them (#3459). Types only: the field's name, type and wire format are unchanged. The server previously subtracted the consumed overage a second time when computing this value, so the number a client reads is larger than before for accounts that ran past their included allowance.
[0.9.0] - 2026-08-20
Removed
-
BREAKING: the
cookiesandheadersrequest-body fields are gone from every capture endpoint (#3242) —/v1/scrape,/v1/scrape/bulk,/v1/screenshots,/v1/recordingsand/v1/schema. A request that carries either field is rejected with400 validation_error; it is not ignored, and there is no deprecation window, feature flag, or compatibility header.ScrapeOpts.cookies,ScrapeOpts.headersand the exportedScrapeCookietype are removed from the SDK surface, so a call that still passes them no longer compiles.// before — compiled, and the values were forwarded to the target site await client.scrape.create({ url, cookies: [{ name: 'session', value: 'abc' }] }); // after — does not compile; the same body over raw HTTP is a 400 validation_error await client.scrape.create({ url });ByteKit continues to manage its own cookie jar and request headers internally; what was removed is the caller's ability to inject or override them.
Changed
-
The generated types now declare the
503response ofGET/POST /v1/fetch(#3372)./v1/fetchanswers503on two conditions and both carry the ordinaryErrorenvelope, so a typed consumer readingoperations['getFetch']['responses']/['postFetch']can handle it instead of meeting an undeclared status at runtime. Readerror.codeto tell the two apart, because they disagree abouterror.http_status:upstream_error— the target returned HTTP502, relayed at503because Cloudflare replaces a clean origin 502 with its own error page.error.http_statusstays502, the true upstream status, and is the only surviving record of what the target answered.internal_error(Fetch service unavailable.) — the fetch service was unreachable. There is no upstream, soerror.http_statusreports503, matching what was served.
No runtime behaviour changes, and every other documented
/v1/fetchstatus is unaffected.
[0.8.0] - 2026-08-19
Removed
-
BREAKING:
billing_multiplierandbilled_bytesare gone from the scrape success envelope (#3143). Readbillinginstead — aBillingNodethat names every factor applied to the charge, the multiplier they compose to, and the resulting billed bytes:// before const spend = res.billed_bytes; // flat, base-endpoint only // after const spend = res.billing?.billed_bytes; const why = res.billing?.factors; // [{ name: 'endpoint', value: 1.5, reason: 'scrape_md' }, …]This is a correction, not a rename: the two surfaces reported different numbers for the same charge. The flat pair carried the BASE-endpoint composition, so a markdown cache hit read
0.5/400while the real charge — and theX-Billing-Multiplierheader — was0.75/600; an html-only miss read the per-format credits estimate1.5/150against an endpoint factor of1.0/100. The flat pair also never carried theclean_markdown×3 surcharge, so on a successful clean it under-reported the amount billed by two thirds.billing.multipliercomposes every applied factor behind exactly one rounding, so it is the number that predicts your invoice.billingis optional and is absent on resources that completed before the billing node existed. A pre-node row omits the field rather than reporting a zero node, because a zero node would assert that a real past charge was zero. Test the node for presence (res.billing !== undefined, or equivalently a plain truthiness check —billingis an object orundefined, and an object is always truthy, so the two are the same test here). The hazard is a condition that reaches into the node:if (res.billing?.billed_bytes)silently skips a genuine zero-byte charge, because0is falsy. Check that the node is present, then readbilled_bytesfrom it.// wrong — a real 0-byte charge is falsy and disappears if (res.billing?.billed_bytes) record(res.billing.billed_bytes); // right — presence first, value second if (res.billing) record(res.billing.billed_bytes); // 0 is a charge, not an absenceTwo endpoints deliberately carry no node:
/v1/searchis billed per credit and reportscredits_used, and a single/v1/fetchreports its charge through response headers only. The/v1/logsrow field namedbilling_multiplieris a different field and is not the envelope field this release removed. It carries the BASE settledcredits_chargedfactor, not the composed real-endpoint value theX-Billing-Multiplierheader reports — a markdown cache hit reads0.5on the logs row and0.75on the header.
Changed
- Generated
ScrapeErrorEnvelopetypes now describe terminal asynchronousscrape.failedwebhook delivery (#1856). No runtime SDK behavior or API shape changed.
Fixed
-
GET /v1/logsentries now carrybilling_multiplier, as a JSON number (#3022). The gateway had been serializing that value under an undeclared keycredits, and as the raw PostgresNUMERIC(10,2)string ("1.50"), while the OpenAPI spec — and therefore the generated types insrc/generated/api.d.ts— declaredbilling_multiplier?: number | null. Anything typed against this package readundefinedfor the field.Breaking at the wire level: the undeclared
creditskey is gone from every/v1/logsentry. Code that reached past the generated types to readcreditsmust readbilling_multiplier. No SDK method exposes this endpoint, so the only change inside this package is the regenerated type description. -
change_threshold(onmonitors.create/monitors.list/monitors.get/monitors.updateresponses) andchange_pct(onmonitors.captures.listentries) now serialize as JSON numbers, not strings (#3021). Both are PostgresNUMERIC(5,2)columns; the gateway passed the raw driver string through unconverted ("5.00"/"7.25") while the OpenAPI spec's request schemas already declarednumber— only the response schemas had been (incorrectly) edited to match the buggy string output. This release fixes the gateway serializer and corrects the two response schemas tonumber/number | null, so spec and runtime now agree.Breaking at the wire level:
client.monitors.create(),client.monitors.list(),client.monitors.get(), andclient.monitors.update()now return a JS number forchange_thresholdinstead of a string (screenshot-type monitors only); the create response'sMonitorCreateResponseand every other monitor route serialize through the sameMonitorResponseschema, so all four are affected identically.client.monitors.captures.list()now returns a JS number forchange_pctinstead of a string. Code doingparseFloat(monitor.change_threshold)or template-literal interpolation keeps working; stricttypeof x === 'string'checks or string methods (.trim(),.padStart()) will now throw.change_pctisnull(not0) when there is no prior capture to diff against, unchanged from before. No SDK method-level shape changed otherwise — the only change inside this package is the regenerated type description.
[0.7.4] - 2026-08-19
Changed
gen:versionno longer runs as part ofbuild(#3144).scripts/gen-version.tswritessrc/version.ts, a tracked source file, and it was chained into thebuildscript. That made a build silently rewrite committed source whenever the file disagreed with the manifest: local runs went green on a tree CI could fail, the resultingM src/version.tswas indistinguishable from routine build noise, and in CI the outcome depended on whether Turbo's build cache hit (restoringdist/**without re-running the generator) or missed.buildnow starts atrm -rf dist ..., so building this package touches nothing tracked. This changesprepack/prepublishOnlybehavior too: both callbuild, so the publish path now reads the committedsrc/version.tsrather than regenerating it, and the committed value is what ships.- New
gen:version-checkscript,pnpm gen:version && git diff --exit-code src/version.ts, mirroring the existinggen:types-checkin the same manifest. It runs in.woodpecker/ci.yaml'stypes-driftstep, so a manifest bump without the matchingsrc/version.tsregeneration now fails a named PR check instead of reachingstaging.src/__tests__/version-parity.test.tsstays in place as the in-suite assertion; the gate is what makes its failure reachable before merge rather than after.
No runtime or type changes. SDK_VERSION and the X-ByteKit-Client: sdk_ts/<version> affix
behave exactly as before.
[0.7.3] - 2026-08-18
Changed
XFetchContentLength's description no longer calls the header a wire-byte count (#3133).X-Fetch-Content-Lengthis the byte length of the response body actually served: on a passthrough/v1/fetch(noformat) that is the relayed upstream payload, so it does equal the upstream wire bytes — but withformat=htmlorformat=markdownthe gateway overwrites it with the transformed document's length, which is typically far smaller than what is billed. The old description therefore read as a billing discrepancy on every transform request. Billing is unchanged and has always used the upstream compressed wire bytes reported byX-Raw-Bytes/X-Billable-Bytes. Generated-type comments only; no type or runtime behavior changes.
[0.7.2] - 2026-08-17
Changed
ScrapeResponses.202's headers no longer advertiseX-Raw-Bytes,X-Billable-Bytes, orX-Billing-Multiplier(#3068). The gateway's async/v1/scrapepath only ever emittedX-Scrape-IDandX-Scrape-Status: queuedon a 202 — a queued scrape has no finalized byte charge yet — butdocs/api/openapi.yamldocumented the billing-transparency trio there too, sosrc/generated/api.d.tstyped three response headers the runtime never sends. This corrects the generated type to match the gateway's actual behavior; it does not change any runtime behavior. The synchronous200response, and/v1/screenshots'202(which does finalize a charge before responding), are unaffected.
[0.7.1] - 2026-08-11
Changed
- Generated types follow the spec's corrected error and format vocabularies (#2959). Two
vocabulary corrections in
docs/api/openapi.yamlflow throughsrc/generated/api.d.ts:ScrapeErrorEnvelope.error.codeno longer listsquota_exhausted. The API has emittedquota_exceededsince #1539 and there is no throw site forquota_exhaustedanywhere in the gateway, so this removes a code that has never been sent — not one that stops being sent. A consumer switching on it was matching a branch that could not be reached; the live code for an exceeded quota isquota_exceeded.invalid_url, which the API does emit, is unchanged and still listed.createBulk'sformatsnow spells the raw-HTML valueraw_html, matchingScrapeRequestandcreateScrapeBulk./v1/bulkpreviously accepted only the internalrawHtmlspelling and the spec documented that; it now accepts both, so the one endpoint that disagreed with the rest of the surface no longer does.rawHtmlkeeps working.
Added
-
ByteKitError.headers— the failing response's headers, so you can write your own backoff (#2957). This SDK performs exactly onefetchper call and never retries, deliberately, so that retry policy stays the caller's. But the policy was unwritable:ByteKitErrorcarried no headers andrequestWithResponsereturns headers only on 2xx — which a rate-limited request never reaches — soRetry-Afterand theX-RateLimit-*family were unreachable through the SDK at every entry point.err.headerscloses that:if (err instanceof ByteKitError && err.status === 429) { const waitMs = Number(err.headers?.get('Retry-After') ?? 1) * 1000; }It is the runtime's own
Headersobject rather than a snapshot, soget()stays case-insensitive whatever casing the wire used. Strictly additive: it is optional and last on the constructor, follows the samedeclaredisciplinedetailsuses, and an error constructed without one carries noheaderskey at all —'headers' in err, spreads andJSON.stringifyare unchanged for every pre-existing construction, as arestatus,code,message,detailsand both 2xx return shapes. Only response headers are exposed;Authorizationrides on the request and is never reachable through it.ByteKitConnectionErroris unchanged — no response, no headers to carry.
Fixed
-
screenshots.getWithResponse's docstring no longer promises headers that endpoint never emits (#2957). The JSDoc — the one an IDE surfaces on hover, and the one the published SDK reference is generated from — claimed the documentedX-Screenshot-*/X-Raw-Bytes/X-Billable-Bytes/X-Billing-Multiplierheaders "are onheaders". They are not:GET /v1/screenshots/{id}is declared with noX-*response headers, which is whatByteKit.requestWithResponse's own doc comment and the README have said all along, and what a live GET confirms. Documentation only — no signature, return type or runtime behavior changed.screenshots.createWithResponse(POST /v1/screenshots) really does emit that family, and its docstring still says so. -
README documents the
signal-vs-timeoutMsprecedence and therequestTimeoutMsescape hatch (#2957). Passing your ownAbortSignalsilently disables the constructor'stimeoutMs— the signal becomes the request's only abort source, so a client built withtimeoutMs: 200waits indefinitely if the signal never fires. That precedence was documented onRequestOptions.signalin the types, where only a reader already inspecting the option would meet it, and nowhere in the README. A new "Timeouts and cancellation" section states it with the full four-row precedence table and showsrequestTimeoutMscomposed alongside a caller signal (the request then aborts on whichever fires first). No behavior changed — the precedence is what it always was. -
ScrapeWarning.codenow carries all 12 codes the server can emit (#2948). The generated union insrc/generated/api.d.tslisted 10.low_quality_extractionandfragment_menu_stripped— added to the server-side emitter union by #1701 — never reacheddocs/api/openapi.yaml, so every regeneration since faithfully reproduced the gap and the exported type was non-exhaustive: aswitchoverwarning.codethat TypeScript proved exhaustive would silently fall through on a warning the API really returns. Widening the spec and regenerating fixes the type; no runtime code changed. The other 10 codes are unaffected. -
Gateway wire-type correction:
billing_multiplieris a JSON number onscrape.get()again (#2954).GET /v1/scrape/{id}(andGET /v1/scrape/bulk/{id}items) were serializingbilling_multiplieras a JSON string (e.g."0.50") instead of a number — the pg driver returns the underlyingNUMERIC(10,2)column as a string, and the gateway's GET serializer passed it through unconverted, whilePOST /v1/scrape's create-path value (computed in JS) was already correct. Fixed upstream in the gateway; this SDK required no code change, only removal of theKNOWN DRIFT (#2426)JSDoc onScrapeResource.get()that documented the workaround — the staticnumbertype this SDK has shipped all along is accurate again. A caller that was defensively coercing withNumber(...)per the old note does not need to change anything; a caller relying on the exact string form"0.50"(rather than doing arithmetic) will now see0.5.
Changed
-
Alignment release — no consumer-visible change in the alignment itself (the
#2948type widening above is a separate, deliberate change under this same version).0.7.0published to npm at2026-08-11T21:17:42.790Z. Two doc comments, insrc/client.tsandsrc/version.ts, were reworded after the commit that introduced the0.7.0version string — they had re-spelled theX-ByteKit-Clientheader name in prose, which issue #2905's pre-merge gate 1 counts. No exported function, type, or runtime behavior differs from0.7.0.This release exists because
check-release-drift(#2680) is version-gated and anchors on the commit that introduced a version, not on the commit whose tree was actually published. Once0.7.0reached npm, any shipped-file change after that anchor reads as post-release drift. Bumping moves the anchor past those comments. The published0.7.0artifact is not stale in any way a consumer can observe — the divergence is comment text only. -
Corrected a false legacy-name claim carried since the rename (#2950). The
0.7.0-era note below said the priorrapidcrawl/@rapidcrawl/sdkname "remains installable at its last version but is deprecated." Verified 2026-08-13: bothrapidcrawland@rapidcrawl/sdkreturn npm404("is not in this registry"), andnpm deprecateagainst either fails with the same404— there is no published version left for a deprecation notice to attach to, so the claim never held. No registry stub or deprecation was possible to publish; rootCLAUDE.mdcarried the same false claim and is corrected alongside this entry. Users of the old names should install@hunt-labs/bytekit-sdkdirectly. No exported function, type, or runtime behavior changes.
[0.7.0] - 2026-08-11
Added
-
Every outbound request now carries
X-ByteKit-Client: sdk_ts/<version>. The SDK identifies itself with the sanctionedclient_surfacemarker (issue #2905, slice 2 of #2701), so requests made through@hunt-labs/bytekit-sdkare distinguishable from raw API calls in ByteKit's own analytics instead of all resolving toapi_direct.sdk_tsis a member of the closedclient_surfacevocabulary declared in issue #2904; the version affix is this package's own version, baked in at build time frompackage.json.Set at exactly ONE place — the header literal inside the private transport every request funnels through — so it rides on the JSON path, the raw
/v1/fetchpath and every resource method alike. Nothing else about a request changed: sameAuthorizationandContent-Type, same body bytes, same client-side abort budget, same error taxonomy, same return shapes. The marker carries no caller data and cannot be set, overridden or read by callers — there is noheadersoption onByteKitOptionsorRequestOptions.Minor rather than patch because the wire format of every request changes, even though no documented API does. A gateway that does not yet recognize the header ignores it.
-
SDK_VERSION— an internal, build-time-generated constant (src/version.ts, written bypnpm gen:versionfrompackage.jsonand gated bysrc/__tests__/version-parity.test.ts). It backs the marker's version affix. Not part of the public export surface;package.json'sversionfield remains the only version consumers should read.
Unchanged
- Still ZERO runtime dependencies. The
client_surfacevocabulary lives in an internal, unpublished workspace package, so the emitted value is a string literal here and the binding to that registry is a test-only import.dependenciesstays empty, asserted bysrc/__tests__/package-manifest.test.ts.
[0.6.2] - 2026-08-11 [NEVER PUBLISHED]
This version was cut but NEVER PUBLISHED — npm latest went from 0.6.1 straight to 0.7.0.
The two documentation/regeneration changes below reached consumers in the 0.7.0 artifact,
which contains all of them. Nothing under this heading is missing from 0.7.0, and nothing is
installable at 0.6.2: npm i @hunt-labs/[email protected] fails.
Why it was stranded: 0.6.2 was cut for two src/generated/api.d.ts regenerations, and the next
change (issue #2905's new outbound request header) altered the wire format of every request —
additive behavior that belongs at a MINOR, not a patch. Folding it into the still-unpublished
0.6.2 would have shipped a new request header to consumers pinning ^0.6.1 unannounced, so 0.7.0
was cut instead and this heading annotated per ci/scripts/check-release-drift.sh's
reconciliation form (#2680).
Documentation
AccountSessionResponse.has_activitynow documents what it actually means. The generated type's JSDoc previously said the flag was true once the account had "ever recorded billable activity", which read as "ever made a request". It is true only once a request has successfully completed and been charged credits — a request that failed does not flip it, even when its bandwidth was billed (issue #2890). The doc comment also now states the all-time (never windowed) scope and the up-to-one-rollup lag, so a caller polling it after a first request knowsfalsecan mean "not visible yet" rather than "never made a request". Regenerated fromdocs/api/openapi.yaml; the emitted TYPE is unchanged (has_activity: boolean), so this release is source-compatible in both directions.
Changed
-
Regenerated
src/generated/api.d.tsfromdocs/api/openapi.yamlafter issue #2904 declared the optionalX-ByteKit-Clientrequest header ascomponents.parameters.ClientSurfaceMarker. The regeneration is ADDITIVE and touches no method signature: one entry appears undercomponents["parameters"], because the parameter is deliberately$ref-ed from no operation. No consumer-visible change — no exported function, type or runtime behavior differs from 0.6.1. The entry exists becausecheck-release-drift(#2680) requires a version bump for any post-release change to a shipped file, and the publish pipeline is version-gated.Documented under 0.6.2 alongside the #2890 regeneration above, rather than under a 0.6.3 of its own, because 0.6.2 has been CUT BUT NOT PUBLISHED (npm
latestis 0.6.1). Both regenerations therefore ship in the same single artifact. Numbering this 0.6.3 would leave 0.6.2 documented and unpublishable — the version-gated pipeline would publish 0.6.3 and skip it — which is precisely the phantom-version class #2680 exists to kill, andcheck-release-driftrejects it by name.
[0.6.1] - 2026-08-10
Changed
- Version-alignment release after the 2026-08-10 publish rescue: the published 0.6.0 artifact was built from the
mainpromotion HEAD, which already contained every change documented under the 0.6.0 heading — but that commit postdates the one that introduced the 0.6.0 version string, so thecheck-release-driftgate (#2680) correctly reported shipped drift on every subsequent CI run (#2829). 0.6.1 realigns manifest ↔ registry ↔ version-introducing commit. No consumer-visible change relative to the published 0.6.0.
[0.6.0] - 2026-08-07
Added
-
ByteKitError.details— the API error envelope's structured rejection reason is now reachable from the thrown error. A422 validation_errorcarries{ formErrors, fieldErrors }, so a caller can finally read WHICH request field was rejected rather than only that the request was invalid; a400 invalid_urlcarries a flat{ url, field, reason }. Typed asByteKitErrorDetails | undefinedwhereByteKitErrorDetails = Record<string, unknown>(also exported), deliberately loose because the contents vary by error code — narrow the member you read. Strictly ADDITIVE:detailsis an optional FOURTH constructor argument, so existing three-argumentnew ByteKitError(status, code, message)construction is unchanged, andstatus/code/messageare byte-identical. Populated only from the canonical{ error: { … } }envelope — a flat vendor-masked body (/v1/search502 {"error":"search_provider_error"}) carries no structured detail and grows none.http_statusandfailed_atare deliberately NOT exposed:http_statuswould duplicate the existingstatusproperty, and a duplicated field that can disagree with its twin is a bug surface. (#2685) -
A
tsc --noEmit-checked parity pin insrc/types.tsties the hand-authored search types (SearchOpts,SearchResult,SearchResultImage,SearchResponse— the SDK's only hand-authored request/response types) to the generatedoperations['createSearch']request and200response. It asserts key-set and optionality parity in both directions and assignability in each meaningful direction, so a future spec change that search's types do not follow fails the build instead of shipping. Value types are erased before the parity comparison, so a deliberate hand-authored refinement (a branded id, a narrowed union) cannot trip it. Type-level only — the assertions erase at emit and add nothing todist/. (#2678)
Fixed
-
README: the
fetchsample'sres.cachetype is corrected to includenull. The documented type read"hit" | "miss" | "bypass", butFetchResult.cacheisFetchCacheStatus | null(null when the response carried no recognizableX-Fetch-Cacheheader) — the one strict-tscerror a real consumption file copied from the README ever produced. The line is now a TYPED ASSIGNMENT rather than a comment, so the README compile guard (src/__tests__/readme-compiles.test.ts) typechecks it: comment-vs-type drift on this field now fails the build instead of shipping. Documentation only — no runtime or type change. (#2685) -
CHANGELOG: the
[0.1.0]heading's malformed- 2026date is now- 2026-04-24, the commit date ofcaf17e56b, which introducedpackages/sdk-typescript/package.jsonat version0.1.0. Every other heading in this file already carried a full ISO date; this one broke the Keep a Changelog## [x.y.z] - YYYY-MM-DDform. (#2685) -
Test-only: nine SDK test files mocked wire bodies the API can never send. A
monitors.create200 (the operation declares 201 as its only success arm), a scrape 200 carryingdata: { markdown }(the envelope has nodatamember — content lives underformats), ascreenshots.getWithResponsemock assertingX-Screenshot-*headers thatGET /v1/screenshots/{id}declares on no arm, error bodies omitting theErrorschema's requiredstatus: 'failed'/http_status/failed_at, and twelve402/429arms on operations that declare neither status. Each is rewritten to a spec-valid shape with a comment naming the arm it now matches, and every test keeps its original assertion target. No shipped code changed. (#2685) -
SearchResult.snippetis now optional (snippet?: string) — BREAKING AT COMPILE TIME.type: 'images'search results omitsnippetentirely (the spec has always declared a result'srequiredset as[position, title, url]), but the SDK typed it as a requiredstring.hit.snippet.lengththerefore compiled under strict TypeScript and threwTypeError: Cannot read properties of undefinedon every images hit — the TypeScript twin of the Python SDK's #2579 imagesKeyError. Consumers reading.snippetunguarded now seestring | undefinedand must narrow it (if (typeof hit.snippet === 'string')orhit.snippet ?? ''), exactly as they already do fordate. No runtime behavior and no wire bytes change: the field was already absent from those payloads.web/newsresults are unaffected — they still carrysnippet, and a narrowed read yields the same string. (#2678) -
Path parameters are now percent-encoded. Every id-bearing method built its request path with a raw template literal, so a crafted or interpolated id silently retargeted the request:
scrape.get('sc_/../../account')addressedGET /v1/accountonce WHATWG URL parsing collapsed the../segments, andmonitors.get('mon_x?admin=1#frag')turned the id into a real query string plus fragment. The branded id types constrain only the PREFIX of an id, never its tail, so they were never a defence. An id now travels as one percent-encoded path segment — a malformed one 404s server-side instead of reaching a different resource. Ports the Python SDK's #2593 fix. (#2677)
Changed
-
Request paths are assembled by a single internal
buildPath()helper inclient.tsrather than by per-resource template literals. It encodes SEGMENTS only — every fixed segment and every/separator stays literal — and encodes each value exactly once, so an id that already contains a%is not double-encoded. A well-formed id (sc_<hex>,mon_<hex>, …) produces a byte-identical URL to before, on every id-bearing method: encoding is a no-op for those characters, so no existing call changes on the wire. (#2677) -
Four of the guards shipped above could not fail on the change they appeared to protect, and now can.
buildPath()'s twoTypeErrorthrows — the fail-loud that replaced the compile-time checking a template literal used to give — each survived its own removal with the whole suite green, and are now pinned by tests separated by input shape rather than by message text. The in-suite path-helper gate read its directory non-recursively while the shell grep it reproduces is-r, so an offender one directory down was invisible to it; it now walks the whole ofsrc/, a strict superset of the shell gate's set. Theimagessearch-result literal's key ABSENCE (nosnippet, nodate) was held only by a one-directional subtype check, which an extra key cannot break; it is now pinned by explicit key-absence assertions plus an exact key-set assertion. And the spec'serror.details— read at one site inclient.tsoff a hand-written inline type — is now tied to the generated schema by a PRESENCE pin, so dropping the field fromdocs/api/openapi.yamlfails the build. Every pin is key-level, never value-level, so a deliberate hand-authored value refinement still cannot trip it. Test- and type-level only: no runtime behavior, no exported type, and nothing indist/changes. (#2793)
[0.5.0] - 2026-07-29
Added
src/generated/api.d.tsnow models the400responses that the gateway's new request-body-size guards can return.operations['stripeWebhook']['responses']gains a400with this route's bespoke{ error?: string }body, andoperations['retryWebhookDelivery']['responses']gains a400carrying the unifiedcomponents['schemas']['Error']envelope. Both are purely additive — no existing member changed shape — so every current call site still compiles. (#2640)
Changed
- Regenerated
src/generated/api.d.tsagainst the merged spec. Beyond the two added400members above, the delta is JSDoc only: every guarded endpoint's description now carries a**Limits:**block stating its byte ceiling and therequest_too_largeerror code (#2640), andPOST /v1/bulk's description documentsdefaults.typeinheritance and the 422 on unrecognized keys (#2643). No runtime code changed. (#2640, #2643)
[0.4.0] - 2026-07-24
Added
RequestOptions(per-callrequestTimeoutMs+ callersignal) is now accepted by every resource method, not justscrape.create/scrape.get. It is always a trailing optional parameter — onscreenshots.createit is the third positional, afterparams— so every existing call site compiles and serializes byte-identically. Per-call options continue to beat the constructor'stimeoutMsdefault; omitting them leaves the constructor default in force exactly as before. (#2547)ApiResponse<T>({ data, headers, status }), a genericclient.requestWithResponse<T>()transport, and three typed siblings —sitemap.getWithResponse,screenshots.createWithResponse,screenshots.getWithResponse— make the documentedX-Raw-Bytes,X-Billable-Bytes,X-Billing-MultiplierandX-Screenshot-*response headers reachable. This surface is purely additive: it issues the identical request with the identical options as the base method, and no existing return type changes. (#2547)MonitorStatusandScrapeCookieare exported from the package entrypoint. (#2547)
Changed (BREAKING at compile time)
ScrapeOpts.cookiesis nowScrapeCookie[], derived from the spec'sCookieschema (nameandvaluerequired;domain,path,secure,httpOnlyoptional), instead of an open bag of untyped records. A cookie missingnameused to compile and fail as a 422 on the wire; it is a compile error now. Serialization is unchanged. (#2547)ListMonitorsParams.statusis now the spec enum ('active' | 'processing' | 'paused' | 'cancelled' | 'suspended'), derived from the generatedlistMonitorsquery type, instead ofstring. A typo like'activee'used to compile and 422; it is a compile error now. The serialized query string is unchanged. (#2547)
Fixed
-
timeoutMs/requestTimeoutMs(and a caller-suppliedsignal) now cover the response body read, not just the time to first byte of headers. The abort budget was armed forfetchand cleared the moment headers arrived, after whichrequest()'sresponse.text(),requestRaw()'sresponse.arrayBuffer(), and the error-bodyresponse.json()on a non-2xx all read the body unbounded — so a server that answered with headers and then stalled its body hung the caller forever. A mid-body abort now surfaces asByteKitConnectionErrorwithcode: 'timeout'(budget) or'aborted'(caller signal) instead of a bareTypeError: terminated.The default is unchanged at 120 s, and the precedence table for
signal/requestTimeoutMs/timeoutMsis untouched. One consequence worth planning for: a legitimately large body (a multi-MB/v1/fetchdocument) now counts against that same budget, where previously only the wait for headers did. (#2546) -
The README's webhook-retry sample used an invalid delivery id prefix (
whd_) that its own branded type rejects. The sample is corrected, and a new test compiles every TypeScript sample in the README against the SDK's sources so a non-compiling sample can no longer ship. (#2547)
Fixed (BREAKING at compile time)
fetch.getno longer accepts the eleven markdown-tuning options (markdown_mode,markdown_query,markdown_links,markdown_images,with_links_summary,with_images_summary,markdown_compact,markdown_filter_images,markdown_include_media,markdown_include_warnings,markdown_include_stats). The spec'sGET /v1/fetchdeclares onlyurl,format,country,timeout_msandcache_ttl, so those options were silently dropped by the query builder — a caller asking formarkdown_mode: 'llm'compiled and received default article-mode markdown. They are now compile errors instead of silent no-ops; usefetch.create(POST), which accepts and serializes all of them unchanged.FetchGetOptsis now derived from the generatedgetFetchquery type, so future spec drift failstsc --noEmitrather than reaching production. (#2539)
[0.3.0] - 2026-07-21
Removed (BREAKING)
- The deprecated
RapidCrawl,RapidCrawlError, andRapidCrawlConnectionErrorexports and theRapidCrawlOptionstype alias (kept as a one-minor-version bridge in #2421) are removed. Import theByteKit*names instead. (#2473) - The
RecordingIdtype export is removed. Recordings are deactivated (/v1/recordingsreturns 410), so the branded id had no reachable use. Breaking in theory only — unused in practice. (#2481)
Fixed
fetch.create/fetch.getno longer throw a bareSyntaxErroron every non-JSON body./v1/fetchreturns the fetched content AS the response body (metadata inX-Fetch-*headers), but both methods routed through the JSON-parsing transport. They now use a raw-capable transport and resolve to a typedFetchResult— rawbody, byte-exactbytes, plusid,creditsCharged,cache,custom,contentType, andheadersparsed from theX-Fetch-*metadata. (#2466)- A trailing slash on the constructor
baseUrl(e.g."https://api.bytekit.com/") no longer produces double-slash request URLs — it is normalized once, preserving a path-carrying baseUrl. (#2481) SearchResult.dateis now optional (string | null | undefined):imagessearch results omit the field entirely. (#2481)
Added
- New
usageresource (get,daily,byEndpoint—GET /v1/usage,/v1/usage/daily,/v1/usage/by-endpoint) andwebhooksresource (list,retry—GET /v1/webhook-deliveries,POST /v1/webhook-deliveries/{id}/retry), with their request/response types exported. (#2471) ScrapeOptsandFetchOptsnow cover the full documented request field set (10 previously missingScrapeRequestfields; 11 markdown-tuningFetchRequestfields), plus the'text'markdown-links mode. A compile-time drift guard keeps them aligned with the generated OpenAPI schema. (#2475)monitors,sitemap,bulk, andfetch.bulkmethods are now fully typed from the generated OpenAPIoperations(previouslyRecord<string, unknown>in /Promise<unknown>out); their request/response types are re-exported. (#2481)monitors.listaccepts typed query params and returns a typed response (ListMonitorsParams/ListMonitorsResponse, both exported). (#2480)
Changed (BREAKING)
fetch.getnow rejects acustomoption at compile time:GET /v1/fetchhas nocustomquery param, so the value was silently dropped before. Usefetch.create(POST) to echo acustompayload. (#2481)FetchOpts.formatis narrowed fromstringto the spec union'markdown' | 'html'; off-spec values now fail to compile. (#2475)
[0.2.2] - 2026-07-16
Changed
- Re-release of 0.2.1. The 0.2.1 artifacts never reached npm — the prod publish's
publish-sdkstep failed on a CI test-isolation flake (cjs-require.test.tsrebuilding the shareddist), which is now fixed. No consumer-facing changes beyond 0.2.1.
[0.2.1] - 2026-07-16 [NEVER PUBLISHED]
Never published to npm. This version was cut in-tree but no
@hunt-labs/[email protected]tarball exists —npm i @hunt-labs/[email protected]fails. Everything below shipped to users in 0.2.2, which is the first published release containing it. Reconciled by issue #2680; therelease-driftCI gate now blocks a new phantom heading from being introduced.
Changed
- Primary exports renamed
RapidCrawl→ByteKit,RapidCrawlError→ByteKitError,RapidCrawlOptions→ByteKitOptions. The old names remain as deprecated, identity-preserving aliases (RapidCrawl === ByteKit), so existing imports keep working. (#2421)
Fixed
request()now returnsundefinedon204 No Content/ empty-body responses instead of throwing a JSON-parse error —account.apiKeys.revoke()now resolves as itsPromise<void>contract declares. (#2450)
Removed
client.recordings(the/v1/recordingsendpoint is permanently410deactivated). (#2421)
[0.2.0] - 2026-07-15
This is a breaking release. Four runtime behavior changes affect code written against 0.1.0 — read each one before upgrading.
Changed (breaking)
- Default client-side timeout (120s). Requests now abort after 120000 ms by
default (
DEFAULT_TIMEOUT_MSinsrc/client.ts); 0.1.0 had no client-side timeout and would hang indefinitely. A slow request now rejects withRapidCrawlConnectionErrorcarryingcode: 'timeout'. Override per client vianew RapidCrawl({ apiKey, timeoutMs }). - Network failures throw
RapidCrawlConnectionError. Everyfetch()rejection is now wrapped bytoConnectionError()and re-thrown asRapidCrawlConnectionError. 0.1.0 surfaced undici's bareTypeError: fetch failed. Code that matched on a bareTypeErrorto detect a network failure will no longer match — catchRapidCrawlConnectionErrorinstead. - Constructor throws on an empty
apiKey.new RapidCrawl({ apiKey: '' })(or a missing key) now throws aTypeErrorimmediately instead of silently sendingAuthorization: Bearer undefined. EnsureapiKeyis populated (e.g. fromprocess.env.BYTEKIT_API_KEY) before constructing the client. - Request-body types narrowed to the OpenAPI schema. Request payloads are now
typed from spec-derived types generated from the OpenAPI schema
(
src/generated/api.d.ts) rather thanRecord<string, unknown>. Payloads that previously compiled by virtue of the loose index type may now be rejected by the type checker; align them with the documented request shape.
Changed
- Package renamed to
@hunt-labs/bytekit-sdk(the@bytekitnpm scope was unobtainable).The priorCorrected in 0.7.1 (see above): that claim does not hold — both names 404 on npm and there is no published version left to deprecate.rapidcrawl/@rapidcrawl/sdkname remains installable at its last version but is deprecated. - Generated API types (
dist/generated/api.d.ts) are now shipped in the tarball so consumers get the typed request/response surface without regenerating from the spec.
Added
- Publish-ready packaging metadata: types-first
exports,engines,keywords,homepage,bugs,README.md,LICENSE, and an MITlicensefield.
[0.1.0] - 2026-04-24
Added
- Initial release of the ByteKit TypeScript SDK — a typed client for the ByteKit API
(scrape, screenshot, search, and related endpoints), published under the original
@rapidcrawl/sdkname.
bytekit-sdk
Python SDK, published on PyPI. Import name: bytekit.
Unreleased
0.9.2
Release cut 2026-10-02.
Changed
- Account
terms_versiondocstring describes the new onboarding contract (#5055) — an empty string now means the account has not accepted the Terms yet (bootstrap no longer records acceptance;POST /v1/account/accept-termsdoes). Docstring only, no model field changes. Regenerated fromdocs/api/openapi.yaml.
0.9.1
Release cut 2026-09-30.
Changed
- Bulk job models add
total_billed_bytes(#5021) — newUnion[None, int]on the bulk job response models (CreateBulkResponse202,GetBulkResponse200/202,DeleteBulkResponse200, and the create/get models of/v1/scrape/bulkand/v1/fetch/bulk) and onBulkCompletedWebhook: the bandwidth billed for the job, in bytes, after multipliers.Noneon the create response and for a bulk that predates the field.total_credits_chargedand the bulk itemcredits_chargedare deprecated in the spec and report0under thedualandbytesbilling modes. Additive, patch-level. Regenerated fromdocs/api/openapi.yaml.
0.9.0
Release cut 2026-09-24.
Changed
CreateSearchResponse502is renamedCreateSearchResponse503(#4891, BREAKING), andCreateSearchResponse502Erroris renamedCreateSearchResponse503Error. The gateway now answers a search-provider failure with HTTP503instead of502, because Cloudflare replaced the502body with its own plain-text page and thesearch_provider_errorcode never reached the caller. The body shape is unchanged. Regenerated fromdocs/api/openapi.yaml.get_fetchandpost_fetchno longer declare a502response (#4891). The gateway relays a target's502or504at HTTP503, with the true upstream status kept in the body'serror.http_status, and the503branch already parsed thatErrorenvelope. A502is now undocumented, so the default client raisesbytekit.errors.UnexpectedStatusfor it. Regenerated fromdocs/api/openapi.yaml.
0.8.2
Release cut 2026-09-23.
Changed
AccountSessionResponse.onboarding_completed(#4780) — newbool: whether the account has left dashboard onboarding. Additive, patch-level. Regenerated fromdocs/api/openapi.yaml.
0.8.1
Release cut 2026-09-18.
Fixed
ListBulkScreenshotsResponse200ItemsItemType0.billing(#4582) — now typedUnion[BillingNode, None, Unset]. The gateway now returnsbilling: nullon every bulk item whosestatusis notcompleted, so an item keeps the same keys on every poll; the previous model passed thatnulltoBillingNode.from_dictand failed to parse the page. A completed item still carries its node, and a completed item from before the node existed still omits the key (UNSET). Regenerated fromdocs/api/openapi.yaml.
0.8.0
Release cut 2026-09-14.
Added
ScrapeRequest.solve_challenge(#4448) — optionalbool, defaultFalseon the server. WhenTrue, the scrape attempts to clear a Cloudflare challenge; success is not guaranteed. It is billed as a normal browser render. Regenerated fromdocs/api/openapi.yaml; a request built without it serializes with nosolve_challengekey, so existing calls are unchanged on the wire.GetUsageResponse200.credits_scrapeandGetUsageDailyResponse200DataItem.credits_scrape(#4452) — requiredint: credits drawn under the scrape usage source, such as/v1/schemaextractions. The gateway always charged this bucket but never returned it, so the published per-source credit fields could not add up tocredits_used. Regenerated fromdocs/api/openapi.yaml. The field is required, so a response from a gateway that predates it raisesbytekit.errors.UnexpectedStatus(with theKeyErroras its cause) on the default client, and returnsNonewhen the client is built withraise_on_unexpected_status=False.
Changed
ScrapeErrorEnvelopeandcreate_scrapedocstrings (#4454) — no longer say an inlinePOST /v1/scrapefailure carriesid: null. The gateway now records a sync scrape that endsupstream_errororblockedas a failed scrape job, so thefailedenvelope carries that job'ssc_id andGET /v1/scrape/{id}returns the same envelope. Regenerated fromdocs/api/openapi.yaml; model fields and operation signatures are unchanged.
0.7.0
Release cut 2026-09-12.
Added
BulkCompletedWebhook/BulkCompletedWebhookItemsItem(#4340) — the body ByteKit POSTs to your endpoint withX-ByteKit-Event: bulk.completed, importable frombytekit.models. The schema has always been indocs/api/openapi.yaml, butopenapi-python-clientemits models only for schemas reachable from a generated OPERATION body, and no operation$refs this one — it describes a request we make to your server, not a response you read from ours. The generator therefore emitted nothing and every Python caller re-derived the body type by hand. The model is hand-written underscripts/hand_written/and installed after each regeneration; a spec-reading drift test (tests/test_bulk_completed_webhook_model.py) fails the build if the spec's required keys and the model's diverge.
Changed
-
Bulk list-item models are now a two-member union, and the old single item class is gone (#4340, BREAKING). A terminal bulk item arrives on the wire in one of two disjoint shapes: an ordinary rehydrated item carrying
status, or the bounded 16-MiB fallback record carryingterminal_outcome. The spec described both with one flat object, so the generated model made every terminal signal optional and a caller could not tell which field to read.GET /v1/fetch/bulk/{id}(200 and 202) andGET /v1/bulk/{id}/screenshotsnow declare it asoneOf: [ordinary, fallback]— matching howGET /v1/scrape/bulk/{id}already declared it — and the generator emits one class per alternative:Removed Replaced by GetFetchBulkResponse200ItemsItemGetFetchBulkResponse200ItemsItemType0(ordinary) andGetFetchBulkResponse200ItemsItemType1(fallback)GetFetchBulkResponse202ItemsItemGetFetchBulkResponse202ItemsItemType0/…Type1ListBulkScreenshotsResponse200ItemsItemListBulkScreenshotsResponse200ItemsItemType0/…Type1The per-field enum classes follow the same rename (
…ItemsItemStatus→…ItemsItemType0Status,…ItemsItemTerminalOutcome→…ItemsItemType1TerminalOutcome).Upgrading:
itemsis nowlist[…Type0 | …Type1]. Code that readitem.statusstill works on the ordinary alternative; branch on the member you got —isinstance(item, …ItemsItemType0)— or read the terminal signal asgetattr(item, "status", None) or item.terminal_outcome.terminal_outcomeoutranksstatuswhen a record carries it, and neither is ever absent.
0.6.0
Release cut 2026-09-11.
Changed
-
create_scrapenow raises on a terminal scrape failure, even though it arrives on a200(#1870). The synchronousPOST /v1/scrapeused to answer an unreachable host, a refused connection, or a bot-protection block that defeated every proxy tier with a503. It now answers200carrying the canonicalfailedenvelope, because a failed fetch is a result rather than a transport error. Left alone, the generated union would have parsed that envelope and RETURNED it on the same arm a caller reads.formatsfrom, so the most common first-call shape — "it returned, so it worked" — would have silently succeeded on a failed scrape.create_scrape.sync/.sync_detailed/.asyncio/.asyncio_detailedtherefore raiseerrors.UnexpectedStatuswith.status_code200and.code/.messageread off the envelope'serror; underraise_on_unexpected_status=Falsethey returnNone, as for any other unusable body.The carve-out is that one call site.
get_scrapestill RETURNS the identical envelope as a typedScrapeErrorEnvelope— polling a job that failed is a successful poll — and every documented 4xx/5xx oncreate_scrapestill returns its typedErrormodel.Upgrading: if you inspected the result of
create_scrapeforstatus == "failed", move that handling into anexcept errors.UnexpectedStatusblock. Code that already caughtUnexpectedStatusneeds no change.
0.5.4 [NEVER PUBLISHED]
Release cut 2026-09-06.
Never published to PyPI. Publication is triggered by CI on
mainfrom the versionpyproject.tomldeclares, and 0.6.0 (#1870) superseded 0.5.4 in the tree before any release reachedmain— nobytekit-sdk0.5.4 artifact exists, sopip install bytekit-sdk==0.5.4fails. Everything below shipped to users in 0.6.0, the first published release containing it.
Added
- Bulk fetch, scrape, and mixed-result models now publish cursor pagination, stable item
identities, whole-job counters, exact mixed artifact keys, artifact warnings, bounded
webhook recovery metadata, and the
artifact_unavailableerror contract (#3717). Generated bulk read functions accept optionalcursor,limit, andstatusparameters.
Fixed
- Corrected the artifact-URL fallback docstrings on
list_bulk_screenshots(image_url,content_url,markdown_url) and the/v1/logsrow model (artifact_url) (#3717). Those fields were documented as null-plus-artifact_unavailablewhen no signature can be minted; the servers have always returned the row's stored value unchanged instead —Nonewhile nothing is stored, and otherwise the private unsigned storage URL, which answers401— and raise no warning.get_fetch_bulkdoes null the field and reportartifact_unavailable, and its docstring is unchanged. No server behaviour changed and no model moved; only the documentation on them did.
Changed
- Regenerated
GetUsageResponse200against the correctedtopup_balance_bytesdescription (#4251). The field's docstring read "Remaining purchased top-up bytes", which contradicted its own "Equal totopup_bytes" clause: the value covers purchased top-up lots and the one-time starter grant, which is what the endpoint has always returned. The clause recording that the figure is already net of consumed overage, and the "Present only in dual/bytes billing mode" clause, are both unchanged and still true. Types are unchanged and no server value changed; only the documentation on them moved. - Regenerated
ScrapeSuccessEnvelopeagainst the clarifiedcache_age_sdescription (#4237). The field's docstring now records thatGET /v1/scrape/{id}reportscache: hitwithoutcache_age_s: the age is a property of the replay that happened at completion time, is not persisted on the row, and cannot be derived honestly afterwards. The one-directional "Present only oncache: hit" clause is unchanged and still true. Types are unchanged; only the documentation on them moved. - Regenerated
ScrapeSuccessEnvelopeagainst the completedcachepresence list (#4203). The field's description now namesGET /v1/scrape/{id}as a third delivery path, alongside the synchronous response and webhook delivery: a polled scrape now reports the cache disposition its row recorded, and omits the field — rather than defaulting it tomiss— for a row that recorded none. Types are unchanged; only the documentation on them moved. - Regenerated
MonitorCreateRequestScrapeOptionsagainst the correctedscrape_optionsdocumentation (#4024). The description no longer listsheadersamong the accepted fields —POST /v1/monitorsnever honoured a caller-suppliedheadersoption and now rejects it with the same 400 every other capture endpoint returns for the field. Types are unchanged; only the documentation on them moved.
0.5.3
Release cut 2026-09-05.
Changed
- Regenerated
ScrapeQueuedEnvelopeagainst the correctedstatus_urldocumentation (#3925).status_urlis a root-relative path, never an absolute URI: join it onto the API base URL you called (https://api.bytekit.com, no trailing slash) to get the URL to poll. The spec had declared itformat: uriwith an absolute example, which no server has ever emitted; the emitted value is unchanged, so no client behaviour moves.
0.5.2
Release cut 2026-09-03.
Changed
-
Breaking (API): the scrape
formatsvocabulary is nowraw | markdown | links | images(#3775).ScrapeRequestFormatsItemdrops the two retired members and gainsRAW, and the response model exposesScrapeFormats.rawin place of the old raw-HTML and cleaned-HTML fields. The server accepts no alias, so a request naming a retired value returns422:body = ScrapeRequest(url="https://example.com", formats=[ScrapeRequestFormatsItem.RAW]) resp = create_scrape.sync_detailed(client=client, body=body) print(resp.parsed.formats.raw)
Fixed
-
402 Payment Requirednow returns a typedErrorinstead of raisingUnexpectedStatusoncreate_scrape,get_fetch,post_fetch,create_fetch_bulk,create_scrape_bulk,create_bulkandcreate_sitemap(#3784). Those operations enforce quota but never declared402in the spec, so the generated client had no arm for it whilecreate_searchandcreate_screenshotdid — the same exhausted account raised on one call and returned a parsed error on the next. Regenerated from the corrected spec:resp = create_scrape.sync_detailed(client=client, body=body) if resp.status_code == 402: print(resp.parsed.error.code) # quota_exceeded | spending_cap_reached | …
Changed
- The generated
eventsfield docstring onScrapeRequestnow documents the widened server default (queued,completed,failed) and that a caller may send an explicit subset to receive fewer deliveries (#3777). Types/docs only — this regeneration followed a server-side default change; the SDK carried no logic tied to the old default.
0.5.1
Release cut 2026-08-25.
Fixed
-
GetScrapeBulkResponse200now exposes theitemsa completed scrape-bulk poll returns (#3632).get_scrape_bulkparsed the terminal200into a model with noitemsfield, so a create-then-poll flow surfaced typed envelopes on every non-terminal poll and lost them on the terminal one, where the results actually live:polled = get_scrape_bulk.sync(id=job.id, client=client) for item in polled.items: # typed ScrapeSuccessEnvelope / ScrapeErrorEnvelope / ScrapeQueuedEnvelope ...Server behavior is unchanged — the field was always on the wire.
-
Polling a bulk job that is still running no longer raises
UnexpectedStatus: 202(#3533).get_scrape_bulk,get_fetch_bulkandget_bulkreturn HTTP 202 with the job envelope while the job is non-terminal and 200 once it is terminal, but only the 200 was documented, so the first poll of the obvious create-then-poll workflow raised (or returnedNonewithraise_on_unexpected_status=False). Each of the three now parses the 202 into its own typed model —GetScrapeBulkResponse202,GetFetchBulkResponse202,GetBulkResponse202— which carries the same fields as the matching 200 model:job = create_scrape_bulk.sync(client=client, body=body) polled = get_scrape_bulk.sync_detailed(id=job.id, client=client) # polled.status_code is 202 while processing, 200 once terminal; .parsed is typed in bothServer behavior is unchanged — this is a client-side parsing fix.
Changed
GetUsageResponse200.topup_balance_bytesnow documents itself as the remaining purchased top-up bytes, already net of any overage consumed against them (#3459). Documentation only: the field's name, type and wire format are unchanged. The server previously subtracted the consumed overage a second time when computing this value, so the number a client reads is larger than before for accounts that ran past their included allowance.
0.5.0
Release cut 2026-08-20.
Removed
-
BREAKING: the
cookiesandheadersrequest-body fields are gone from every capture endpoint (#3242) —/v1/scrape,/v1/scrape/bulk,/v1/screenshots,/v1/recordingsand/v1/schema. A request that carries either field is rejected with400 validation_error; it is not ignored, and there is no deprecation window, feature flag, or compatibility header.ScrapeRequest.cookies,ScrapeRequest.headers, the matchingScreenshotRequestfields and the generatedCookiemodel are removed from the package.# before client.scrape(url=url, cookies=[Cookie(name="session", value="abc")]) # after — the keyword no longer exists; the same body over raw HTTP is a 400 validation_error client.scrape(url=url)ByteKit continues to manage its own cookie jar and request headers internally; what was removed is the caller's ability to inject or override them.
Changed
-
fetchnow parses a503response instead of treating it as unexpected (#3372)./v1/fetchanswers503on two conditions and both carry the ordinaryErrorenvelope, so the generated client returns anErrorfor them rather than raisingUnexpectedStatus(or returningNone) the way an undeclared status is handled. Readerror.codeto tell the two apart, because they disagree abouterror.http_status:upstream_error— the target returned HTTP502, relayed at503because Cloudflare replaces a clean origin 502 with its own error page.error.http_statusstays502, the true upstream status, and is the only surviving record of what the target answered.internal_error(Fetch service unavailable.) — the fetch service was unreachable. There is no upstream, soerror.http_statusreports503, matching what was served.
Nothing else about the call changes, and every other documented
/v1/fetchstatus is unaffected.
0.4.0
Release cut 2026-08-19.
Removed
-
BREAKING:
billing_multiplierandbilled_bytesare gone fromScrapeSuccessEnvelope(#3143). Readbillinginstead — aBillingNodethat names every factor applied to the charge, the multiplier they compose to, and the resulting billed bytes:# before spend = res.billed_bytes # flat, base-endpoint only # after spend = res.billing.billed_bytes # guard on isinstance(res.billing, BillingNode) why = res.billing.factors # [BillingFactor(name="endpoint", value=1.5, reason="scrape_md"), ...]This is a correction, not a rename: the two surfaces reported different numbers for the same charge. The flat pair carried the BASE-endpoint composition, so a markdown cache hit read
0.5/400while the real charge — and theX-Billing-Multiplierheader — was0.75/600; an html-only miss read the per-format credits estimate1.5/150against an endpoint factor of1.0/100. The flat pair also never carried theclean_markdownx3 surcharge, so on a successful clean it under-reported the amount billed by two thirds.billing.multipliercomposes every applied factor behind exactly one rounding, so it is the number that predicts your invoice.billingisUNSETon resources that completed before the billing node existed. A pre-node row omits the field rather than reporting a zero node, because a zero node would assert that a real past charge was zero. Test withisinstance(res.billing, BillingNode), never with a truthiness check — a legitimate zero-byte charge is a value, not an absence.Two endpoints deliberately carry no node:
/v1/searchis billed per credit and reportscredits_used, and a single/v1/fetchreports its charge through response headers only. The/v1/logsrow field namedbilling_multiplieris a different field and is not the envelope field this release removed. It carries the BASE settledcredits_chargedfactor, not the composed real-endpoint value theX-Billing-Multiplierheader reports — a markdown cache hit reads0.5on the logs row and0.75on the header.
Changed
- Generated
ScrapeErrorEnvelopedocumentation now describes terminal asynchronousscrape.failedwebhook delivery (#1856). No runtime SDK behavior or API shape changed.
Fixed
-
GET /v1/logsentries now carrybilling_multiplier, as a JSON number (#3022). The gateway had been serializing that value under an undeclared keycredits, and as the raw PostgresNUMERIC(10,2)string ("1.50"), while the OpenAPI spec declaredbilling_multiplieras a number.Breaking at the wire level: the undeclared
creditskey is gone from every/v1/logsentry. Code readingcreditsoff a logs response must readbilling_multiplier. No SDK method exposes this endpoint, so nothing inside this package changed. -
change_thresholdonMonitorResponse/MonitorCreateResponseandchange_pctonCaptureResponseare now typedfloat, notstr(#3021). Both are PostgresNUMERIC(5,2)columns; the gateway passed the raw driver string through unconverted ("5.00"/"7.25") while the OpenAPI spec's request schemas already declarednumber— only the response schemas had been (incorrectly) edited to match the buggy string output. This release fixes the gateway serializer and corrects the two response schemas, so spec and runtime now agree.Breaking at the wire level:
bytekit.api.monitors.create_monitor,bytekit.api.monitors.get_monitor,bytekit.api.monitors.list_monitors, andbytekit.api.monitors.update_monitornow returnfloatforchange_thresholdinstead ofstr(screenshot-type monitors only,create_monitor/update_monitorincluded since both also serialize through the sameMonitorCreateResponse/MonitorResponseschemas);bytekit.api.monitors.list_monitor_capturesnow returnsfloatforchange_pctinstead ofstr.change_pctisNone(not0.0) when there is no prior capture to diff against, unchanged from before. Only the generated model description changed inside this package.
0.3.9
Release cut 2026-08-11.
Changed
-
The declared
httpxfloor is raised from>=0.24.0to>=0.28.0(#2998). The SDK's own tests assert the exact request and response bytes it puts on the wire, and those bytes changed in httpx 0.28.0, which switched to compact JSON separators (b'{"a":"b"}'; every release up to and including 0.27.2 emitsb'{"a": "b"}'). The old floor therefore promised an install the suite cannot run green: on httpx 0.24–0.27 the dependency resolves, and 12 assertions then fail with a message naming neither httpx nor a version. Raising the floor was chosen over loosening the assertions, because the compact form is the wire format the SDK actually emits and pinning it is the reason those tests exist. No runtime behaviour of this package changes — only whatpipwill resolve. httpx 0.28.0 declaresrequires-python >=3.8, so this excludes no Python version the SDK supports (requires-python = ">=3.10"). -
Generated vocabularies follow the spec's corrected error and format enums (#2959).
ScrapeErrorEnvelopeErrorCodeno longer carriesQUOTA_EXHAUSTED: the API has emittedquota_exceededsince #1539 and no gateway throw site producesquota_exhausted, so this removes a member that has never arrived over the wire — not one that stops arriving. Code matching on it was matching a branch that could not be reached; the live code for an exceeded quota isquota_exceeded.INVALID_URL, which the API does emit, is unchanged.CreateBulkBodyDefaultsFormatsItemandCreateBulkBodyItemsItemFormatsItemrenameRAWHTML = "rawHtml"toRAW_HTML = "raw_html", matchingScrapeRequestand/v1/scrape/bulk./v1/bulkaccepted only the internal spelling and the spec documented that; it now accepts both, so the one endpoint that disagreed with the rest of the surface no longer does.rawHtmlkeeps working on the wire.
Fixed
-
An invalid
typeordate_rangepassed toclient.search(...)now raises an error you can act on (#2958).search(type="bogus")raised a bareValueError: 'bogus' is not a valid CreateSearchBodyTypestraight out of a generated enum constructor — it named a class you never imported, it did not say which values would have worked, and the method'sRaises:section listed onlyUnexpectedStatus, so the error was undocumented as well as unhelpful. The message now names the parameter, the value it rejected and every value it accepts, and it is raised before any request is sent, so a typo can never be billed.The type hints for both arguments narrowed from
Optional[str]to aLiteralunion with the generated enum class, which makes a wrong CONSTANT a type error at the call site rather than a runtime surprise. This is hints-only: anystris still accepted at runtime, so code that computes the value — from argv, a config file, a database column — keeps working unchanged. Enum members (CreateSearchBodyType.IMAGES) type-check as before. -
A successful 200 carrying an extraction-quality warning no longer fails to parse (#2948).
ScrapeWarningCodewas generated from a spec enum that listed 10 of the 12 codes the server emits;low_quality_extractionandfragment_menu_stripped(added server-side by #1701) were missing.ScrapeWarning.from_dictbuilds the code withScrapeWarningCode(...), which raisesValueErroron an unknown member, and #2684'sbytekit_parseguard then turned that into the two-tier contract — so a successful, billed 200 reached the caller aserrors.UnexpectedStatusunder the defaultraise_on_unexpected_status=True, and asresponse.parsed is Noneunder the opt-out. Both codes are now in the spec and the enum, so such a response parses normally. Callers who added a defensiveexcept UnexpectedStatusaround scrape calls for this reason can drop it. The other 10 codes are unaffected.
Changed
-
Connection failures are documented. Every
Raises:section namedhttpx.TimeoutExceptionand stopped there, and the README's error-handling section did the same. Its transport siblinghttpx.ConnectError— DNS failure, connection refused, TLS handshake failure — is just as reachable and is caught nowhere, soexcept (UnexpectedStatus, httpx.TimeoutException)written from the documented surface left an uncaught exception waiting for the first flaky network. Both client-class docstrings,search()'sRaises:section and a new README Transport errors table now name both. No behavior changed: neither exception was ever caught, and neither is now. -
README: two first-use traps.
get_fetchtakesurl_query=, noturl=(it travels as theurlquery parameter), andAuthenticatedClient.search(...)is synchronous — calling it inside a coroutine blocks the event loop, withcreate_search.asyncioas the async alternative. Both are now documented with runnable examples. -
The sdist no longer ships a legacy-brand name.
bytekit-sdk==0.3.8carried one occurrence of the pre-cutover project name — inside a.gitignorethis package does not own and its build configuration does not list. hatchling force-includes the nearest ignore file into an sdist, which here means the repository's, so the string reached PyPI while a search of the package itself came back clean. The line is corrected, and the BUILT tarball is now grepped on every test run rather than the source tree. Nothing you install changes; the artifact simply stops advertising a name and a command that no longer exist. -
A release can no longer be published while its own changelog denies it exists. The
0.3.8sdist shipped with its## 0.3.8section still carrying the cut-time placeholder that denies publication: it was written when the release was cut, was never updated when the release actually happened, and nothing existed to notice. The publish step now refuses to upload a version whose section still carries that placeholder. This entry documents the fix for readers of the artifact; the guard itself lives in the repository. (The placeholder's own wording is deliberately not quoted here — the guard reads this section, and quoting it would trip the very check being described.) -
Alignment release — the published
0.3.8artifact already carries this content.0.3.8published to PyPI at2026-08-11T21:18:32.641Z(sdistbytekit_sdk-0.3.8.tar.gz), built from the promotion HEAD, so it contains every change documented under0.3.8— including the two review fix-ups to the injectedclient_surfaceconverter (non-mappingheaders=values, and case-insensitive override precedence). Verified by downloading the published sdist and readingsrc/bytekit/client.py. Nothing about the installed package changes through this alignment — the#2948enum fix above is a separate, deliberate change riding the same unpublished version.This release exists because
check-release-drift(#2680) is version-gated and anchors on the commit that introduced a version, not on the commit whose tree was actually published. Those two fix-ups landed after the commit that introduced0.3.8, so once0.3.8reached PyPI the gate read them as post-release drift. Bumping moves the anchor past them. This mirrors the0.3.6version-alignment release cut after the 2026-08-10 publish rescue.
0.3.8
Published to PyPI 2026-08-11 (21:18:32.641Z).
Added
-
Every request now carries an
X-ByteKit-Clientheader identifying this SDK and its version —X-ByteKit-Client: sdk_python/<version>, sent by default from bothClientandAuthenticatedClient, on the synchronous and the asynchronous httpx client alike. It lets ByteKit tell traffic that came through the Python SDK apart from direct API calls; before this release those were indistinguishable. The version half resolves from the installed distribution's metadata (the same source asbytekit.__version__), so it can never drift frompyproject.toml.A caller-supplied header of the same name wins, whether it is passed to the constructor (
Client(headers={"X-ByteKit-Client": "…"})) or merged in later (client.with_headers({"X-ByteKit-Client": "…"})) — the marker is a telemetry dimension, not an authentication or authorization control, and ByteKit treats an unrecognized value exactly as it treats a missing one. Unrelated caller headers leave the marker intact.The override is matched case-insensitively, as HTTP header names are, so
x-bytekit-clientandX-BYTEKIT-CLIENToverride just asX-ByteKit-Clientdoes — including throughhttpx.Headers, which lowercases every key it is given. Exactly oneX-ByteKit-Clientheader goes on the wire either way; you will never see your override and the default sent together.Nothing else about request construction changed:
AuthenticatedClientstill stamps its auth header,with_headersstill merges caller keys over defaults, the cookies, timeout, verify and redirect settings of both clients are untouched, andheaders=still accepts everything it accepted before — a dict,None, a list or tuple of pairs, anhttpx.Headers.
Fixed
- A
headersdict you pass to a client is no longer written into by the SDK. Previously the client kept your mapping by reference and stampedAuthorization: Bearer <your token>into it when it built its httpx client, so a dict you still held — and might log, reuse for a second client, or share across threads — silently acquired your API key. The client now copies what you pass, so your object is left exactly as you gave it and two clients built from one dict can no longer contaminate each other's credentials. Nothing about the headers the client sends changed.
0.3.7 [NEVER PUBLISHED]
Never published to PyPI. This version was cut in-tree but no
bytekit-sdk0.3.7 artifact exists —pip install bytekit-sdk==0.3.7fails. Everything below shipped to users in 0.3.8, which is the first published release containing it.
Documentation
AccountSessionResponse.has_activitynow documents what it actually means. The generated model's attribute docstring previously said the flag was true once the account had "ever recorded billable activity", which read as "ever made a request". It is true only once a request has successfully completed and been charged credits — a request that failed does not flip it, even when its bandwidth was billed (issue #2890). The docstring also now states the all-time (never windowed) scope and the up-to-one-rollup lag, so a caller polling it after a first request knowsFalsecan mean "not visible yet" rather than "never made a request". Regenerated fromdocs/api/openapi.yaml; the attribute's TYPE is unchanged (has_activity: bool), so this release is source-compatible in both directions.
0.3.6
Documentation
- No README change ships in 0.3.6. The README's Error handling documentation reached PyPI inside the published 0.3.5 artifact (2026-08-10 publish rescue) and stays documented under the 0.3.5 heading, which is the version whose wheel actually carries it.
Changed
- Version-alignment release after the 2026-08-10 publish rescue: the published 0.3.5 artifact was built from the
mainpromotion HEAD, which already contained every change documented under the 0.3.5 heading — but that commit postdates the one that introduced the 0.3.5 version string, so thecheck-release-driftgate (#2680) correctly reported shipped drift on every subsequent CI run (#2829). 0.3.6 realigns manifest ↔ registry ↔ version-introducing commit. No consumer-visible change relative to the published 0.3.5.
0.3.5
Breaking
AuthenticatedClienttakes no positional arguments.token,prefixandauth_header_nameare now keyword-only, joiningbase_url,timeout,raise_on_unexpected_statusand the rest — soAuthenticatedClient(...)accepts keywords only. Before 0.3.0 the first positional argument wasbase_url, which madeAuthenticatedClient("https://api-stg.bytekit.com", "sk_live_…")a documented call. When 0.3.0 madebase_urla defaulted keyword argument, that call stopped meaning what it said and started binding the URL totokenand the API key toprefix— sendingAuthorization: sk_live_… https://api-stg.bytekit.comto the default host,https://api.bytekit.com, with nothing raised and nothing warned. It now raisesTypeError. Migration is mechanical:AuthenticatedClient(base_url="https://api-stg.bytekit.com", token="sk_live_…"). Keyword construction — the form the README, the docs site and every example already use — is entirely unchanged, includingprefix=/auth_header_name=overrides and thewith_headers/with_cookies/with_timeouthelpers. The unauthenticatedClientwas already keyword-only and is unaffected.
Fixed
- A schema-drifted response body no longer crashes with a bare
KeyError. A generated model'sfrom_dicttrusts therequiredkeys the OpenAPI spec declared when the client was generated, so a server that renamed, dropped or retyped a field leaked the generator's own internal exception straight to the caller —KeyError: 'schema_version'fromcreate_scrape,KeyError: 'period_start'fromget_usage,KeyError: 'data'fromlist_monitors, plusTypeError/ValueErrorfor non-object and nested-drift bodies. It happened on a documented status (a200/202), in both raise modes, and no row of the documented error table covered it, so a caller who handled that table faithfully still had no branch for it. Schema drift now takes exactly the same two arms as a non-JSON body:errors.UnexpectedStatus(carrying.status_codeand the raw.content, with the original parse failure attached as__cause__) in the default mode, andNoneunderraise_on_unexpected_status=False. Deliberately no new exception type — a new class would be one every existingexcept errors.UnexpectedStatussilently fails to catch, i.e. the same untyped crash in a different costume. Bodies that match the documented schema parse exactly as before, and a genuinely optional key that is absent still yieldsUNSET. client.search()never returns a silentNone. Its 200 body is parsed through the same shared guard the generated operations use, which honorsraise_on_unexpected_status— so an opt-out client gotNonehanded back from a non-JSON 200 or a schema-drifted one, contradicting both the method's non-Optionalreturn annotation and its own docstring.search()now raiseserrors.UnexpectedStatusin both raise modes.raise_on_unexpected_statusconfigures the generated two-tier contract, whose signatures areOptional[...];search()was never part of it — it already raises on documented error statuses that the generatedcreate_searchreturns as a typedErrormodel. A well-formed 200 still returns the parsedCreateSearchResponse200in either mode.- A non-IANA HTTP status no longer crashes every operation with a bare
ValueError. Each generated operation built itsResponsewithstatus_code=HTTPStatus(response.status_code)evaluated before the response was parsed, so any status outside Python'shttp.HTTPStatus— Cloudflare's520–530family, nginx's499— raisedValueError: 520 is not a valid HTTPStatusfrom inside the SDK. This happened in both raise modes: the defaultraise_on_unexpected_status=Truenever got to raise itserrors.UnexpectedStatus, andraise_on_unexpected_status=False, which promisesNone, raised theValueErrortoo.api.bytekit.comis served through Cloudflare, so this was exactly the edge/CDN failure class the typed-error contract exists for. All 29 operations now behave as documented:errors.UnexpectedStatus(carrying.status_codeand the raw.content) in the default mode, aResponsewith.parsed is Nonein the opt-out mode. The hand-writtenclient.search()wrapper was never affected and is unchanged.
Changed
-
Response.status_codeis a plainintfor a statushttp.HTTPStatusdoes not know. This is the contract decision behind the fix above, recorded here because it is the one observable difference in the typed surface. Concretely:- Every status the stdlib enum does know — documented by the API or not,
200as much as503— still comes back as a realhttp.HTTPStatusmember, soresponse.status_code.phrase,.name, and identity comparisons such asresponse.status_code is HTTPStatus.OKkeep working exactly as before. - A status the enum does not know (
499,520,521,522,530, …) comes back as the plainintthe wire carried.intcomparisons (response.status_code == 520,>= 500) work; enum-only attributes (.phrase,.name) do not exist on it. The alternatives were rejected deliberately: a syntheticIntEnummember would invent a.phrase/.nameno registry backs, and raising before theResponseis constructed is the bug itself — it is precisely whatraise_on_unexpected_status=Falsepromises not to do. - The declared annotation on
Response.status_codeis unchanged (HTTPStatus), so this release adds no type errors to existing consumer code; widening it toUnion[HTTPStatus, int]would make.phrase/.namea type error on every response, including the IANA ones, which is a far larger break than the residual it would close. Reach for.phrase/.nameonly after anisinstance(..., HTTPStatus)check if you handle edge statuses off a non-raising client.
In the default (raising) mode this is largely invisible: a non-IANA status raises
errors.UnexpectedStatusbefore anyResponseis returned, andUnexpectedStatus.status_codehas always been a plainint. - Every status the stdlib enum does know — documented by the API or not,
Documentation
- The "Error handling" example in the README now requests
formats=[…MARKDOWN]explicitly. Run literally, its success arm printed<bytekit.types.Unset object at 0x…>: the snippet builtScrapeRequest(url=...)with noformats, so the server applied itsraw_htmldefault andresult.formats.markdownwas legitimately unset. The same class of defect 0.3.3 fixed in the quick start, in the block directly beneath it. The README's error table gains the schema-drift row, a note thatsearch()is outside the two-tier contract, and a migration note for the keyword-only constructor.
0.3.4
Breaking
-
CreateBulkBody,CreateBulkBodyDefaults,CreateBulkBodyItemsItem, andCreateFetchBulkBodyUrlsItemType1lost their additional-properties mapping API.docs/api/openapi.yaml's/v1/bulkrequest schema declaresadditionalProperties: falseon the top-level body,items[], anddefaults(issue #2641), and/v1/fetch/bulk's per-item override schema insideurls[]declares the same (issue #2648), matching the gateway's own.strict()validators. The code generator responds toadditionalProperties: falseby omitting the catch-all mapping it otherwise attaches to every generated model, so these four models no longer exposeadditional_properties, theadditional_keysproperty,__getitem__,__setitem__,__delitem__, or__contains__. Code that didbody["some_key"] = value,"some_key" in body, or readbody.additional_keyson any of these four models will now raiseAttributeError/TypeErrorinstead. Use the model's declaredattrsfields directly instead (e.g.CreateBulkBodyItemsItem(url=..., type=...),CreateFetchBulkBodyUrlsItemType1(url=..., format_=...)); the server itself now rejects unrecognized keys on these endpoints with422 validation_error, so the removed escape hatch could never have reached the API successfully anyway. This is scoped to the fourcreate_bulk_body*/create_fetch_bulk_body_urls_item_type_1models — no other generated model is affected. -
If you are upgrading from 0.3.2 or earlier, note that 0.3.3 also carried a breaking model removal that went undocumented at the time — see the "Breaking" entry under 0.3.3 below (
CreateFetchBulkBodyMetadataandCreateScrapeBulkBodyItemsItemMetadata).
0.3.3 [NEVER PUBLISHED]
Never published to PyPI. This version was cut in-tree but no
bytekit-sdk0.3.3 artifact exists —pip install bytekit-sdk==0.3.3fails. Everything below shipped to users in 0.3.4, which is the first published release containing it. Reconciled by issue #2680; therelease-driftCI gate now blocks a new phantom heading from being introduced.
Breaking
CreateFetchBulkBodyMetadataandCreateScrapeBulkBodyItemsItemMetadatawere removed; both fields now use the sharedMetadatamodel. This landed with the default-stripping traversal fix listed under "Fixed" below (issue #2592) and was not recorded at the time. Once server defaults stopped being materialized into the request-direction schemas, the two per-bodymetadataobjects became structurally identical to the sharedMetadatacomponent, and the code generator emits one model per distinct schema — sobytekit.models.create_fetch_bulk_body_metadataandbytekit.models.create_scrape_bulk_body_items_item_metadatano longer exist and importing either raisesModuleNotFoundError.CreateFetchBulkBody.metadataandCreateScrapeBulkBodyItemsItem.metadataare now typedUnion[Unset, Metadata]. The wire format is unchanged — the same JSON object is sent either way — so the migration is purely at the import site:from bytekit.models.metadata import Metadata, thenMetadata(...)(orMetadata.from_dict({...})) wherever you constructed one of the two removed classes.
Security
- Path parameters are now percent-encoded. All 13 operations that interpolate an id into
their URL (
get_scrape,get_screenshot,get_bulk,delete_bulk,list_bulk_screenshots,get_scrape_bulk,get_fetch_bulk,get_monitor,update_monitor,delete_monitor,list_monitor_captures,get_sitemap,retry_webhook_delivery) previously interpolated the value raw, so an id containing/,?,#or traversal segments could retarget the request to a different path on the same authenticated host — e.g. aDELETE /v1/bulk/{id}becoming a delete against another resource. Values are now encoded withquote(str(value), safe=""). This is not a breaking change for any id the API issues: hex andsc_/ss_/mon_/sm_/bulk_-prefixed ids contain no reserved characters, so the resulting URL is byte-identical to before. No id-format validation was added — the client does not reject id shapes.
Fixed
- Reusing one client across several
asyncio.run(...)calls no longer raisesRuntimeError: Event loop is closed. Anhttpx.AsyncClient's connection pool belongs to the event loop that created it, and the SDK cached its internal async client unconditionally. It now tracks which loop that client was built on and transparently rebuilds it when the running loop changes, re-applyingbase_url, headers, timeout,httpx_argsand authentication. Behavior is unchanged when no loop is running, and a client you supply viaset_async_httpx_client(...)is never rebuilt or closed — it stays yours to manage. See the README's new "Clients and event loops" section. - The README quick start now prints markdown instead of an
Unsetplaceholder. It requested noformats, so the server applied itsraw_htmldefault andresult.formats.markdownwas legitimately unset. It now asks forformats=[ScrapeRequestFormatsItem.MARKDOWN]— noteformatstakes enum members, not plain strings. client.search(type="images")no longer crashes with a bareKeyError: 'snippet'. The OpenAPI spec's search result-item schema declaredsnippetasrequiredwhile its own description said the key is omitted forimagesresults — a self-contradiction the code generator trusted literally, soCreateSearchResponse200ResultsItem.from_dictunconditionally didd.pop("snippet").snippet(likedate) is nowUnion[Unset, str], matching the real gateway behavior (web/newsresults still always carrysnippet;imagesresults omit it entirely) — fixed at the spec level (docs/api/openapi.yaml) and regenerated, not patched in the generated model. (Issue #2579.)- Server defaults are no longer materialized into request wire bodies/query strings
for inline request bodies and
$ref'd parameters.scripts/filter-spec.ts's default-stripping traversal previously only followed$ref'd request-body component schemas, soScrapeRequest/FetchRequestwere clean but every other request-direction shape kept baking in server defaults:/v1/search's fully-inline body (type,limit,country,language,date_range), all three bulk endpoints' bodies and their nestedDefaults/ItemsItemmodels, and$ref'd query parameters (GET /v1/fetch'scountry/timeout_ms/cache_ttl,list_monitors'limit/status,list_webhook_deliveries'/list_monitor_captures'limit,create_screenshot'sasync). The traversal now walks inline request bodies AND parameters (both operation-level and shared path-item-level,$ref'd or inline) before generation, so an omitted optional field always serializes to nothing and the server's own default applies — caller-supplied values are unaffected and still serialize exactly as given (issue #2592). tests/test_no_materialized_defaults.pyrewritten to enumerate every request-direction model and operation parameter programmatically (walking the generatedbytekit.api/bytekit.modelstrees) instead of sampling two known-clean models, so a newly introduced leaker is caught automatically.
Changed
- The source distribution no longer ships
tests/. The bundled suite could not be collected from an unpacked sdist: 12 of its 27 modules require the repository's codegen scripts or the canonical OpenAPI spec, neither of which belongs in a published distribution, and three of them build distributions of the package itself. Nothing importable was removed — the wheel is unchanged,py.typedincluded, sopip install bytekit-sdkis unaffected.
Internal
- CI now proves the committed client is reproducible from the canonical OpenAPI spec, and that the generation chain is deterministic (generating twice yields byte-identical output). This closes the gap where a spec change could land with a TypeScript-only regeneration and leave the Python client silently stranded on an older spec.
ruffis now pinned to an exact version alongside the code generator. The generator runsruffover everything it emits, so it — not the generator alone — determines the committed bytes; the generator itself accepts anyruff<0.13, which meant a fresh install could reformat the whole tree. Bumping the pin is a deliberate, reviewed reformat.- The PEP 561
py.typedmarker is now emitted by the post-generation injector rather than hand-maintained inside the generated tree (the generator omits it under--meta none).
0.3.2
Added
bytekit.__version__reports the installed distribution version, resolved at import time fromimportlib.metadata(so it can never drift frompyproject.toml's[project].version). When the package is imported from a source tree with no installed distribution it reads0.0.0.dev0rather than raising. The dead version override insdk-python-config.yaml— inert under--meta noneand contradicting the real version — has been removed, leaving one authoritative version.
Fixed
AuthenticatedClient.search()is now visible to type checkers. It was attached to the class after creation, so despite the package shipping apy.typedmarker,mypyandpyrightreported"AuthenticatedClient" has no attribute "search". It is now declared as a real method in the class body that delegates to the same implementation: runtime behavior, arguments, return type and raised errors are unchanged — only the static surface is fixed.
0.3.1 [NEVER PUBLISHED]
Never published to PyPI. This version was cut in-tree but no
bytekit-sdk0.3.1 artifact exists —pip install bytekit-sdk==0.3.1fails. Everything below shipped to users in 0.3.2, which is the first published release containing it. Reconciled by issue #2680; therelease-driftCI gate now blocks a new phantom heading from being introduced.
Fixed
- The "never a bare
json.JSONDecodeError" guarantee is now package-wide across all generated operations, not only theAuthenticatedClient.search()wrapper. Every generated operation previously calledresponse.json()unconditionally on a DOCUMENTED status, so a500/502serving an HTML load-balancer page raised a barejson.JSONDecodeErrorfrom inside the SDK. All operations now parse documented statuses through a shared guard: a non-JSON body raises the typederrors.UnexpectedStatus(or returnsNonewhenraise_on_unexpected_status=False), carrying the status code and the raw body. errors.UnexpectedStatus.code/.messageare now populated for generated operations too, from the documentedErrorenvelope — previously they were alwaysNoneoutsidesearch(). A string-valuederror(the masked search-provider502) still never enriches, so the upstream provider's identity is never surfaced.- Behavior on JSON bodies is unchanged: JSON success bodies still parse into their typed
success models, documented JSON error bodies are still returned as typed
Errormodels rather than raised, andraise_on_unexpected_statussemantics are untouched. The documented non-JSON success paths —get_fetch/post_fetch's raw-text200anddelete_monitor's empty204— are likewise unaffected. - README error documentation corrected. The quick-start example wrapped a call in
try/except UnexpectedStatusand then readresponse.formats.markdown, but a documented4xxreturns a typedErrormodel without raising — so the documented example failed withAttributeError: 'Error' object has no attribute 'formats'. The README now documents the real two-tier contract, and the snippet is executed bytests/test_readme_examples.pyso it cannot silently drift again.
Documentation
- Removed the contributor-only
Publishing,Regenerating, andDevelopmentsections from the published README (the PyPI long description) — they described internal CI mechanics and a monorepo-contributor workflow that do not belong on a public package index. The regeneration/development knowledge is preserved in-repo inpackages/sdk-python/CLAUDE.md(not shipped in the distribution). - Added a
## Licensesection and aLICENSEfile, matching the npm sibling packages.
0.3.0
Breaking
- PyPI distribution renamed
bytekit->bytekit-sdk. PyPI administratively denylists the bare namebytekit, so the package installs aspip install bytekit-sdk. The import name is unchanged —import bytekitstill works, and no module path moved. Nothing was ever published under the old distribution name, so there is no migration for existing installs. raise_on_unexpected_statusnow defaults toTrue. Undocumented response statuses now raiseerrors.UnexpectedStatusinstead of silently returningNone. Callers that relied on the oldNone-return behavior must passraise_on_unexpected_status=Falseexplicitly to opt out.base_urlis now a keyword-only argument. It gained a default (https://api.bytekit.com), and to satisfy attrs field ordering it became keyword-only. Any caller passingbase_urlpositionally must switch to thebase_url=keyword form. (base_url=was already the documented usage.)
Added
AuthenticatedClient(token=...)now works out of the box against production:base_urldefaults tohttps://api.bytekit.com.- Finite default request timeout of
120s(httpx.Timeout(120.0)) — requests no longer hang indefinitely. Override withtimeout=. errors.UnexpectedStatusnow carries optional.code/.messageattributes. TheAuthenticatedClient.search()wrapper populates them from the documentedErrorenvelope, and safely raises a typed error (never a barejson.JSONDecodeError) on a non-JSON documented-status body such as an HTML 502.
@hunt-labs/bytekit-cli
Command-line client, published on npm. Binary: bytekit.
[Unreleased]
[0.10.1] - 2026-09-24
Changed
- A
/v1/searchprovider failure now arrives at HTTP503instead of502(#4891): Cloudflare replaced the API's502body with its own plain-text page, so thesearch_provider_errorcode never reached the CLI. The body is unchanged and the CLI already reads it by shape, not status; only a source comment changed in this package.
[0.10.0] - 2026-09-14
Added
--solve-challengeonscrape create(#4449). It sendssolve_challenge: true, which asks the scrape to attempt to clear a Cloudflare challenge; success is not guaranteed. It is billed as a normal browser render. Without the flag the request body is unchanged and carries nosolve_challengekey.
[0.9.4] - 2026-09-10
Added
--cursor,--limitand--statuson the three bulk recovery commands (#3717):bulk screenshots list <id>,scrape bulk get <id>andfetch bulk get <id>. They forward the cursor pagination the API already exposed, so a job whose webhook manifest came back incomplete can be enumerated page by page from the CLI.--limitis bounded 1-500 and--statusaccepts only the documented result statuses; both are rejected locally before a request is made. No new command and no--allflag — the existing commands gained options, nothing else moved.
[0.9.3] - 2026-09-03
Changed
- Breaking (API):
--formatnow takesraw | markdown | links | images(#3775).rawis the renamed unprocessed-source format, and the cleaned-article-HTML format is gone; the server rejects both retired values rather than translating them, so--format raw --format markdownreplaces the old spelling.--raw/-oaccept the two textual formats,markdownandraw.
Fixed
- A failed request's response headers now survive on the thrown
ByteKitError(#3784). The CLI's own copy of the SDK error parser dropped the response after reading its body, soRetry-Afterand theX-Quota-*trio were unreadable on an error even though the gateway sent them. The SDK has passed them into the fifth constructor argument since #2957; this copy now matches, and the parity suite pins the two parsers against each other on a shared header corpus.
[0.9.2] - 2026-08-31
Changed
- Corrected the
detailValueTextdoc comment insrc/output.ts, which claimed the object arm of thedetailsrenderer carried the SAME depth bound as the array arm throughdetailJson(#3691). It carries the same constant (DETAIL_MAX_DEPTH), not the same counter:detailJsontakes no depth argument and starts a fresh budget from its own replacer chain. The comment now records the two consequences — the arms agree level-for-level only on homogeneous chains, and a mixed array/object chain can render up to2 × DETAIL_MAX_DEPTHlevels — and why threadingdepthintodetailJsonis not the fix: it would elide shapes that render today, breaking the byte-identical inverse-case contract #3647 shipped under. No consumer-visible change — this release touches one comment, and everydetailsshape renders byte-identically to 0.9.1.
[0.9.1] - 2026-08-27
Fixed
-
A rejected request now names the field that was rejected. When the API answers
422 validation_error, its envelope carriesdetails.fieldErrors— the field the server refused and what it will accept. The CLI dropped it twice over: its own transport parser never readerror.detailsoff the envelope (the SDK has read it since@hunt-labs/[email protected]), andhandleErrorrendered only the message, code and status. The result wasError: Invalid request body. (validation_error, status: 422)and nothing else, in human and--jsonmodes alike, leavingcurlas the only way to find out which field was wrong (#3538).$ bytekit scrape create --url https://example.com --country zzzz Error: Invalid request body. (validation_error, status: 422) Details: fieldErrors.country: String must contain exactly 2 character(s)The block is rendered on stderr, below the existing one-liner, and is bounded — at most ten lines of at most 200 characters, with a
… and <n> moreline when there are further entries — so an oversized or deeply nesteddetailscan never become a multi-KB dump in a terminal. Nothing else changed: theError:prefix, the(<code>, status: <n>)suffix, exit code1and the silence of stdout are all as before, and an error carrying nodetails(or adetailsthat is not a JSON object) prints exactly the single line it printed in0.9.0.--jsonstdout stays byte-for-byte pipe-clean, because every byte of this goes to stderr.The bound covers depth as well as size: the renderer descends at most eight array levels into a
detailsvalue and elides anything deeper as…. The line and width caps are applied after that walk has finished, so on their own they could not stop adetailspayload nested thousands of arrays deep — a bodyJSON.parseaccepts, so a server can send one — from overflowing the stack and printing a NodeRangeErrortrace in place of the one-line error (#3633). Everydetailsshape the API sends renders byte-identically: nothing real is more than one array level deep.The same bound now covers the object arm of that walk. A
detailsvalue that is a JSON object was serialized with a bareJSON.stringify, which is itself natively recursive and overflowed the stack on its own: flattening the key by one level bounds WHICH values are rendered, not how deep each one is serialized, so a deeply object-nested payload still printed aRangeErrortrace (#3647). Objects nested more than eight levels below the flattened key now elide as…, exactly as over-deep arrays do. Everything shallower renders byte-identically, both live shapes included.
[0.9.0] - 2026-08-20
Removed
-
BREAKING:
--cookiesand--headersare gone fromscrape createandscreenshots create(#3242). The API no longer accepts caller-supplied cookies or request headers on any capture endpoint, so passing either flag is now an unknown-option error before any request is issued, and a body carrying those keys is rejected by the server with400 validation_error. There is no deprecation window, feature flag, or compatibility header.# before bytekit scrape create --url https://example.com --cookies '[{"name":"session","value":"abc"}]' # after — the flag does not exist bytekit scrape create --url https://example.com--webhook-headersonmonitorsis a different flag and is unaffected.
[0.8.1] - 2026-08-13
Fixed
bytekit searchexplains a malformed200instead of leaking aTypeError. A server answering200 {"ok":true}— a proxy, a stub, a future envelope change — drove the human-mode renderer intoError: response.results is not iterable, which named an internal expression rather than the field the body was missing. It now reports, on one line and with no stack trace,search: the response carries no "results" array — the server returned an unexpected 200 body. Re-run with --json to inspect it.and exits1. Aresultsvalue that is present but not an array is caught by the same gate: a string was previously iterated CHARACTER BY CHARACTER, printing one garbled row per character and exiting0. Unchanged: an emptyresults: []is still a successful zero-hit search that prints nothing and exits0; valid responses render byte-identically; and--jsonnever reaches the renderer, so it keeps dumping the raw body at exit0— which is how you inspect an unexpected body.
Added
screenshots … -o <file>now says so when the filename contradicts the captured format.--formatis omitted far more often than not and the server default is jpeg, sobytekit screenshots create --url … -o shot.pngwrote JPEG bytes into a.png-named file silently, at exit0;screenshots gethas no--formatflag at all, so it could not even be told what it was downloading. Both now print one advisory line to stderr —Note: the screenshot was captured as jpeg, but shot.png names .png. The file holds jpeg bytes — pass --format png to capture it as png.The captured format is read from the artifact URL's own path, which is the only place the CLI can observe it. Advisory, not enforcement: the bytes written and the exit code are unchanged, and the note goes to stderr, so-o -pipelines and stdout redirects stay byte-identical. It stays silent unless both the destination and the artifact name a recognized image format and the two differ —.jpgand.jpegare the same format, and an extension-less destination, a non-image extension,-o -, or an artifact URL with no usable extension all make no claim to contradict. A not-ready poll is untouched: it still prints its bare id and poll note at exit2.
Changed
-
The README now documents the whole flag surface, and says which reference wins. Every flag worked and every flag appeared in
--help; the README simply framed subsets as exhaustive, sobulk create's--webhook-secret/--defaults/--metadata,scrape bulk create's--custom,sitemap create's--webhook-url/--webhook-secret/--cache-ttl/--compact/--process/--metadata,monitors createandmonitors update's--cron(required with--interval-type cron) and--metadataplus the rest ofupdate's surface,fetch create's ten markdown flags and--custom, andscreenshots create's--headers/--cookies/--metadatawere all undocumented. All of them are documented now, and a note at the top of the README states that--helpis the authoritative flag reference. Required flags are marked as such —--fileand--webhook-urlon all three bulk creators,--url/--interval-type/--webhook-urlonmonitors create, and--urlonscrape create,screenshots create,sitemap createand bothfetchverbs — so no reader mistakes one for optional. Note in particular that a bulk job has no poll-only mode:--webhook-urlis mandatory there, unlike onsitemap createwhere omitting it really does mean poll-only. A test asserts the parity in the direction that matters — every flag the parser accepts must appear in the README — so this cannot drift again silently. No command, flag or behavior changed. -
Alignment-release note, carried forward from this version's original cut.
0.8.0published to npm at2026-08-11T21:18:01.304Z. Doc comments insrc/client.tsandsrc/package-version.tswere reworded after the commit that introduced the0.8.0version string — they had re-spelled theX-ByteKit-Clientheader name in prose, which issue #2906's pre-merge gate 1 counts. No command, flag, exit code, or runtime behavior differs from0.8.0on account of those comments.0.8.1was cut for that reason becausecheck-release-drift(#2680) is version-gated and anchors on the commit that introduced a version, not on the commit whose tree was actually published. Once0.8.0reached npm, any shipped-file change after that anchor reads as post-release drift. Bumping moves the anchor past those comments.0.8.1had not yet reached npm when the entries above landed, so they ride this same pending release rather than stranding it as a never-published version. -
Corrected a false legacy-name claim carried since the rename (#2950). The
0.8.0-era note below said the priorrapidcrawl-cliname "remains installable at its last version but is deprecated." Verified 2026-08-13:rapidcrawl-clireturns npm404("is not in this registry"), andnpm deprecate rapidcrawl-cli@'*'fails with the same404— there is no published version left for a deprecation notice to attach to, so the claim never held. No registry stub or deprecation was possible to publish (npm has nothing to act on); rootCLAUDE.mdcarried the same false claim and is corrected alongside this entry. Users of the old name should install@hunt-labs/bytekit-clidirectly. No command, flag, or runtime behavior changes.
[0.8.0] - 2026-08-11
Added
-
Every outgoing request now carries
X-ByteKit-Client: cli/<version>. The CLI identifies itself with the sanctionedclient_surfacemarker (issue #2906, slice 3 of #2701), sobytekitusage is distinguishable in ByteKit's own analytics from both raw API calls (api_direct) and programmatic@hunt-labs/bytekit-sdkusage (sdk_ts) instead of being counted as one of them.cliis a member of the closedclient_surfacevocabulary declared in issue #2904; the version affix is this package's own version, resolved frompackage.jsonat runtime — the same valuebytekit --versionprints.Set at exactly ONE place — the header literal inside the private transport core all three CLI transports funnel through — so it rides on every command, on the JSON path, on the raw
/v1/fetchpath, and on the 429 retry attempt alike. Nothing else about a request changed: sameAuthorizationandContent-Type, same body bytes, same per-attempt abort budget, same single-retry 429 behavior, same error taxonomy, same exit codes and output shapes. The marker carries no user data and cannot be set, overridden or read from the command line —--headersis a scrape option that travels in the request body to the captured site, and never touches the CLI's own request headers.Minor rather than patch because the wire format of every request changes, even though no documented command behavior does. A gateway that does not yet recognize the header ignores it.
[0.7.1] - 2026-08-10
Changed
- Version-alignment release after the 2026-08-10 publish rescue: the published 0.7.0 artifact was built from the
mainpromotion HEAD, which already contained every change documented under the 0.7.0 heading — but that commit postdates the one that introduced the 0.7.0 version string, so thecheck-release-driftgate (#2680) correctly reported shipped drift on every subsequent CI run (#2829). 0.7.1 realigns manifest ↔ registry ↔ version-introducing commit. No consumer-visible change relative to the published 0.7.0.
[0.7.0] - 2026-08-07
Breaking
- A not-ready
screenshots get -o/screenshots create -onow exits2and prints the baress_id, instead of exiting1withnot ready yet: screenshot is pending. The two poll commands disagreed:scrape get --rawon a still-queued scrape already wrote the baresc_id to stdout and exited2— a distinct, scriptable "accepted but not ready" signal — while the screenshots side reported the same state as a failure and threw the handle away, so a polling script could branch on "queued" for one command and not the other. The screenshots branch now mirrors scrape exactly: baress_id on stdout, a poll note on stderr, exit2, and (as before) no file written. A script that treated any non-zero exit as "this screenshot failed" now sees2for a job that is merely still running — branch on the code (2= poll again,1= failed) rather than on non-zero. Three states deliberately do not move: a terminallyfailedscreenshot stays exit1with its error surfaced (it will never complete, so a "poll again" signal would loop a script forever), an envelope carrying noss_id stays exit1(there is no handle to hand back), and--jsonkeeps the envelope at exit0. --jsonon afetchcommand now emitsX-Fetch-IDandX-Fetch-URL, notX-Fetch-IdandX-Fetch-Url. The metadata keys were derived by a generic segment-wise Title-Caser, which lowercases every non-initial character of a segment — so the two acronym-bearing headers came out in a spelling the API reference never uses, and ajq '."X-Fetch-ID"'filter written from the docs returnednull. All 16X-Fetch-*keys now match the spec names byte-for-byte. The values are unchanged, the non-X-Fetch-*headers stay filtered out, and every other key (X-Fetch-Waf-Vendor,X-Fetch-Duration-Ms,X-Fetch-Fast-Path, …) is spelled exactly as before. A consumer reading.["X-Fetch-Id"]must switch to.["X-Fetch-ID"].
Fixed
- A
screenshots get -o/screenshots create -opoll only says "poll again" for apendingorprocessingscreenshot. The not-ready predicate decided from the presence of animage_url, anidand a nested error alone —statuswas read only to phrase the message — so acompletedscreenshot whoseimage_urlnever materialized, a status outside the closed enum, and the flaterror_code/error_messagefailed shape all received exit2and "no image artifact is available yet. Poll it with: …" for a job that would never complete. Those states now exit1: a terminal failure printsscreenshot failed: …, and any other artifact-less state printsno image artifact available: screenshot is <status>. Genuinepending/processingpolls keep the baress_id, the poll note and exit2exactly as before — including a poll whose envelope carries anerror_codefrom an attempt that failed and is being retried, since the status, not the error, decides whether polling can still help.--jsonstill prints the envelope at exit0. - A terminally failed screenshot reported through the spec's flat
error_code/error_messagefields now reaches stderr. Only the nested{error:{code,message}}envelope was read, so the shapeScreenshotResponseactually declares surfaced neither code nor message. It now printsscreenshot failed: <message> (<code>)— the formattingscrape get --rawalready uses for a failed scrape. The nested envelope's own message is unchanged. scrape get --rawon a terminally failed scrape now reports why it failed. It printed--raw: response contains no formats— technically true of aScrapeErrorEnvelope, and the one diagnostic that cannot explain anything — while the envelope's ownerror.codeanderror.messagenever reached the caller. Both now go to stderr (exit1, unchanged). The failure is reported ahead of the format diagnostics, so an envelope carrying both a failure and an emptyformats: {}still says why it failed rather than pointing at a--formatchoice that cannot help.- An empty-formats success now names the warning that explains it. A completed scrape whose
persisted artifact could not be rehydrated comes back with
formats: {}and anartifact_unavailablewarning;--rawrendered that as--raw requires --format when the response contains multiple formats, which is false on both counts. It now names the warning's code and message, or says plainly that the formats map is empty when no warning explains it. - Error paths no longer risk losing their own diagnostic.
handleErrorand the missing-API-key path each wrote to stderr and then immediately hard-process.exit(1). Node's stderr is asynchronous when it points at a pipe, andprocess.exitdiscards whatever is still queued — so the one line a script greps was the line most at risk of being thrown away, which is exactly the defect this package's flush policy exists to prevent. Both now setprocess.exitCodeand let the process end naturally once stderr has drained. The exit codes and the rendered messages are byte-identical. --headers,--customand--metadatasaidmust be valid JSON (a object)on a malformed value; the article now agrees —an object/an array.- The
[0.6.0]link reference, missing from the bottom of this file, is restored.
[0.6.0] - 2026-07-29
Breaking
screenshots create --asyncnow exits2in human mode instead of0. The queued 202 is not a completed capture — there is no artifact yet — and every other queued path in the CLI already signalled that with exit code2(scrape create --async). A script that relied on exit0to mean "the screenshot is ready" was reading a false success; underset -esuch a script now stops at the queue step, which is the intended correction. The envelope itself is unchanged and still printed. Two carve-outs:--jsonkeeps exit0(the envelope is a complete machine-readable payload — identical toscrape create --async --json), and--async -o <file>also exits2while still writing no file (it prints the queued envelope and skips the download, as before). Poll withbytekit screenshots get <id>.- A
--fileper-item key outside a bulk endpoint's own strict schema now fails the whole create with a422instead of silently succeeding with that key ignored. In 0.5.x,--filenever read anything buturlfrom a JSON line — every other key was dropped client-side before the request was ever built. As of 0.6.0 those keys ride to the server (itemsonbulk create/scrape bulk create; insideurlsonfetch bulk create), and/v1/bulk//v1/scrape/bulkreject any key their.strict()per-item schema doesn't recognize. A file that worked in 0.5.x purely because its extra keys were quietly discarded may now422— see each command's--filehelp for the keys actually honored on that endpoint. fetch bulk create --fileper-item keys outsideurl/format/country/cache_ttlnow fail the whole create with a422instead of silently succeeding with that key ignored./v1/fetch/bulk's per-item override schema (insideurls[]) is now.strict()on the gateway (issue #2648), matching the/v1/bulk//v1/scrape/bulkposture above. The--filehelp text forfetch bulk createis now uniform withbulk create/scrape bulk createand no longer enumerates keys — this entry is where the honored set (url,format,country,cache_ttl) is documented instead.
Added
- Per-item bulk configuration on all three bulk creates (
bulk create,scrape bulk create,fetch bulk create). A--fileline may now be a JSON object carryingurlplus per-item fields instead of only a bare URL; such a file is sent asitemson/v1/bulkand/v1/scrape/bulk, and as a heterogeneousurlsarray on/v1/fetch/bulk(which has noitemsproperty). A line whose only key isurlstill normalizes to a bare URL string, so a file of url-only lines produces a byte-identical request to before. A JSON line with a missing or non-stringurlis now rejected locally with a usage error (exit1) instead of being sent to the server as a URL. (See Breaking, above, for the per-item strict-schema behavior change this enables.) --defaults,--webhook-secretand--metadataon all three bulk creates, plus--customonscrape bulk createonly (/v1/bulkand/v1/fetch/bulkhave no top-levelcustom, so the flag would be rejected or stripped there).--defaultsis forwarded verbatim; the server owns precedence and unknown-key handling.- The full markdown-tuning surface on
fetch create:--markdown-query,--markdown-links,--markdown-images,--with-links-summary,--with-images-summary,--markdown-compact,--markdown-filter-images,--markdown-include-media,--markdown-include-warnings,--markdown-include-stats, and--custom. - Webhook delivery and processing flags on
sitemap create:--webhook-url,--webhook-secret,--cache-ttl,--compact,--process,--metadata. Webhook-driven sitemap crawls were previously unreachable from the shell. --headers,--cookiesand--metadataonscreenshots create.--metadataonmonitors create, and--metadata+--urlonmonitors update.--typeremains deliberately absent fromupdate— it is immutable server-side.
[0.5.1] - 2026-07-28 [NEVER PUBLISHED]
Never published to npm. This version was cut in-tree but no
@hunt-labs/[email protected]tarball exists —npm i @hunt-labs/[email protected]fails. Everything below shipped to users in 0.6.0, which is the first published release containing it. Reconciled by issue #2680; therelease-driftCI gate now blocks a new phantom heading from being introduced.
Fixed
- Corrects the scope of the 0.5.0 "stalled body" fix claim.
screenshots create/get -odownloads theimage_urlartifact bytes with a bareglobalThis.fetchcall (lib/download-artifact.ts) that sat entirely outside the transport seamsendWithRetryowns — no abort signal, no connection-error taxonomy. The 0.5.0 entry below ("The per-attempt request timeout now covers the response body read... a server that answered with headers and then stalled its body... hung the CLI forever", #2546) reads as if it covered every response body; it only ever covered API-call bodies read through the JSON/raw transports. A stalled artifact host (headers, then a body that never completes) still hung the CLI indefinitely until now. The download path now shares the same per-attempt abort budget andByteKitConnectionErrortimeout taxonomy as the API transport (createAttemptBudget/toConnectionError, exported fromclient.ts— no second timeout implementation), with the same 120 s per-attempt default and no retry (artifact GETs are not the API's 429 domain). Partial-file policy: the response is buffered fully in memory before any write, so an aborted download never leaves a partial/truncated file at the-odestination. User-visible consequence: a legitimately slow (but not stalled) large artifact download now hard-fails after 120 s instead of waiting indefinitely, and there is currently no CLI flag to raise that budget for a single command. (#2587) - While in the file: the 429-retry path left its (unread) response body undrained before sleeping
and retrying — undici cannot reclaim the underlying socket for connection-pool reuse until a body
is fully read. The body is now drained with
response.text()on that path, bounded by its own short (2 s) cap independent of the 120 s per-attempt budget, so a 429 whose body stalls can no longer delay the retry by anywhere near the full attempt budget (or, with the budget disabled, forever). Correction to an intermediateUnreleaseddraft of this same fix: that draft's cap only bounded how long the retry waited — it resolved on schedule but never touched the abandonedresponse.text()call itself, which kept reading against a live socket indefinitely. Because every CLI success path exits via a softprocess.exitCode(#2544) rather thanprocess.exit(), that orphaned read kept the whole process alive forever — strictly worse than having no drain at all, and on the exact class of hang this ticket exists to eliminate. The cap now also aborts the request when it fires, so the abandoned read is actually torn down, not merely ignored; this holds even when the per-attempt timeout itself is disabled (timeoutMs <= 0). User-visible consequence: a 429 with a stalled body still costs the retry at most ~2 s, exactly as before, but the CLI process now reliably exits afterward instead of hanging forever regardless of that visible latency. The same discard-drain on the artifact-download's non-2xx path is removed outright rather than capped: that path throws straight intoprocess.exit(1), so there is no subsequent request in the process for a reclaimed socket to serve, and the drain there bought nothing. (#2587)
[0.5.0] - 2026-07-24
Note
- The
0.3.1and0.4.0versions below document real, tagged changes but were never published to npm — the registry's version history jumps straight from0.3.0to0.5.0. Don't go looking for those tarballs; the changes they describe ship for the first time in this release.
Added
- Surface parity round 2 (#2545): every
x-sdk-scope: v0.1endpoint the SDK exposes now has a CLI command, and the high-value request fields are reachable by flags. Each new flag follows the omit-when-absent rule — absent → the key is left out of the request, so a server default is never materialized.- New command groups:
usage get | daily | by-endpoint(billing-period usage reporting),webhook-deliveries list | retry <id>(inspect and requeue deliveries), andbulk cancel <id>(stop an in-flight bulk job). - Monitor pause/resume:
monitors update --status paused/--status active.monitors create/updatealso gain--change-threshold(float),--notify-on,--webhook-secret, and the JSON-object flags--webhook-headers,--options,--scrape-options.--typeis create-only (a monitor's type is immutable). scrape createmulti-format--format: repeatable AND comma-separated —--format raw_html --format markdownand--format raw_html,markdownboth sendformats: ["raw_html","markdown"]. Raw output (--raw/-o) requires exactly one format. New scrape flags:--cache-ttl,--markdown-query,--cookies,--headers,--delay-ms,--events,--token-encoding,--custom,--remove-base64-images/--no-remove-base64-images(tri-state), and the--markdown-*/--with-*-summaryfamily.scrape get --raw/-o/--format: retrieve a completed queued scrape's body from the shell (a still-queued job prints the id and exits2).screenshots create:--block-ads/--no-block-ads,--block-cookie-banners/--no-block-cookie-banners,--scroll/--no-scroll(tri-state pairs),--wait-for-selector,--delay-ms,--dark-mode,--device-scale-factor,--include-html,--country,--language.fetch create/fetch get:--country,--timeout-ms,--cache-ttl(both verbs);--markdown-mode(create only).sitemap create:--strategy,--max-depth,--max-urls.
- New command groups:
Fixed
-
--jsonno longer emits the literal textundefined— which is not valid JSON — for a response with no body.JSON.stringify(undefined)returns the valueundefined, sobytekit monitors delete <id> --json | jq .failed with a parse error on every delete, and--json -o <file>on the same response crashed withERR_INVALID_ARG_TYPEinstead of writing anything. Every--jsoncode path now serializes through one helper. (#2544) -
scrape create --async --raw(and the bare-oform) no longer risks losing thesc_id it writes. The id went to stdout immediately followed by a hardprocess.exit(2); Node documentsprocess.stdoutas asynchronous when it points at a pipe, so anything still queued at that moment is discarded — exactly thesc_id=$(bytekit scrape create … --async --raw)capture the flag exists for. The queued signal is now a softprocess.exitCode, so the process ends naturally once stdout has drained. The exit code is unchanged. (#2544) -
The per-attempt request timeout now covers the response body read, not just the time to first byte of headers. The abort budget was armed for
fetchand cleared the moment headers arrived, after which reading the body was unbounded — so a server that answered with headers and then stalled its body (including a non-2xx whose error body never finished) hung the CLI forever. The budget now spans headers and body on both transports (requestand the/v1/fetchraw path), and a mid-body abort surfaces as the usualByteKitConnectionErrorwithcode: 'timeout'.The default is unchanged at 120 s, and the budget is still per attempt — the single 429 retry and its backoff sleep still get a fresh full budget. One consequence worth planning for: a legitimately large body (a multi-MB
/v1/fetchdocument) now counts against that same 120 s, where previously only the wait for headers did. (#2546)
Documented
- Output contract: with
--json, a204 No Contentresponse (e.g.monitors delete) renders as JSONnull. Human mode still prints nothing for the same response. (#2544) - Exit codes now have their own README section. In particular exit code
2— "accepted but still queued", emitted byscrape create --asyncin raw and human modes — was previously documented nowhere despite being observable since 0.3.0. It is deliberately neither0(a content-body success) nor1(an error). (#2544) - Corrected two false README claims about
scrapeoutput: a bare-oimplies--rawand writes the requested format's body (it does not write "the same text rendering the terminal would show"), and--async --rawprints the baresc_id on stdout with a note on stderr (it does not print the queued job envelope). (#2544)
[0.4.0] - 2026-07-23 [NEVER PUBLISHED]
Never published to npm. This version was cut in-tree but no
@hunt-labs/[email protected]tarball exists —npm i @hunt-labs/[email protected]fails. Everything below shipped to users in 0.5.0, which is the first published release containing it. Reconciled by issue #2680; therelease-driftCI gate now blocks a new phantom heading from being introduced.
Removed (BREAKING)
-
The entire
bytekit account api-keyscommand group —create,list,get,update,reveal, andrevoke. Every one of these subcommands was structurally unreachable: they target/v1/account/api-keys*, which the API mounts behind a Clerk dashboard session and marksx-internalin the OpenAPI spec. The CLI's only credential is a bearersk_API key, which can never satisfy a session verifier, so all six returned401 unauthorizedin every published version. Running one now exits1witherror: unknown command 'api-keys'.Key management was deliberately NOT moved onto bearer auth. The reveal operation returns the live plaintext of any key on the account, and there is no per-key scope or permission model, so accepting API-key auth there would turn any leaked key into a self-perpetuating credential factory.
Replacement: manage API keys from the dashboard at app.bytekit.com. (#2536)
[0.3.1] - 2026-07-22 [NEVER PUBLISHED]
Never published to npm. This version was cut in-tree but no
@hunt-labs/[email protected]tarball exists —npm i @hunt-labs/[email protected]fails. Everything below shipped to users in 0.5.0, which is the first published release containing it (0.4.0 was never published either — see its heading above). Reconciled by issue #2680; therelease-driftCI gate now blocks a new phantom heading from being introduced.
Fixed
bytekit fetch get/fetch createprint the fetched content again. 0.3.0's claim that fetch "prints the raw body by default, or theX-Fetch-*metadata as JSON with--json" was not true of the shipped artifact: the CLI replaced onlyclient.request, but SDK 0.3.0 moved/v1/fetchontoclient.requestRaw, so the CLI's raw-body branch was unreachable and every fetch fell through to the generic key/value printer — emittingbody: …, a JSON map of every byte underbytes:, andheaders: {}. The CLI now overrides BOTH transport seams and renders the SDK's realFetchResult. Binary content is byte-exact in every mode. (#2535)- Fetch commands honor the CLI's single 429 retry again. Riding the un-overridden SDK transport also bypassed the retry, the per-attempt 120 s timeout, and the connection-error taxonomy; all three now apply to fetch exactly as they do to the JSON commands. (#2535)
Added
-o, --output <file>onfetch get/fetch create: writes the fetched body bytes to a file, byte-for-byte (with--json, writes theX-Fetch-*metadata object instead). (#2535)
[0.3.0] - 2026-07-21
Removed (BREAKING)
- The legacy
RAPIDCRAWL_API_KEYandRAPIDCRAWL_BASE_URLenvironment-variable fallbacks (kept as a one-minor-version bridge in #2421) are removed. UseBYTEKIT_API_KEYandBYTEKIT_BASE_URL(or the--key/--base-urlflags). (#2473)
Fixed
bytekit fetch get/fetch createno longer crash on non-JSON bodies. The transport JSON-parsed every 2xx response, but/v1/fetchreturns the fetched content AS the body. The fetch command now prints the raw body by default, or theX-Fetch-*metadata as JSON with--json;fetch bulkand every other command still parse JSON. (#2467)204 No Content/ empty-body responses (e.g.monitors delete,account api-keys revoke) no longer throw a JSON-parse error, including after a 429 retry. (#2467, #2468)search --type imagesno longer prints anundefinedsnippet line for results that omit the field. (#2479)
Added
scrape createaccepts the fullScrapeRequestsurface:--country,--timeout-ms,--markdown-mode,--async,--webhook-url,--token-budget,--clean-markdown,--include-tags,--exclude-tags,--mobile. Each flag is omitted from the request when absent. (#2480)screenshots createaccepts--device,--viewport WxH,--format,--quality,--wait-until,--clip x,y,w,h, and--async;-o <file>onscreenshots create/getdownloads the captured image bytes (-o -for stdout).-ocannot be combined with--jsonon screenshots. (#2480)monitors listaccepts--limit,--cursor, and--statusquery filters. (#2480)account api-keys reveal <ak_id>: prints the key masked by default (sk_live_…2345); pass--showfor the full plaintext. (#2480)- Per-attempt transport timeout (120s, mirroring the SDK default): a hung
request now fails with
code: 'timeout'instead of hanging forever. Network failures are mapped onto the SDK's connection-error taxonomy (timeout/aborted/connection), and error output renders the API or connection error code. (#2479) - A queued async job (HTTP 202) under
--rawprints the job envelope with itssc_id instead of an empty body. (#2479)
Changed (BREAKING)
fetch --formatis validated locally against the spec union'markdown' | 'html'; off-spec values are rejected with a clear error before a request is sent (previously forwarded as-is). (#2475)
[0.2.2] - 2026-07-16
Changed
- Re-release of 0.2.1. The 0.2.1 artifacts never reached npm — the prod publish's
publish-sdkstep failed on a CI test-isolation flake (cjs-require.test.tsrebuilding the shareddist), which is now fixed. No consumer-facing changes beyond 0.2.1.
[0.2.1] - 2026-07-16 [NEVER PUBLISHED]
Never published to npm. This version was cut in-tree but no
@hunt-labs/[email protected]tarball exists —npm i @hunt-labs/[email protected]fails. Everything below shipped to users in 0.2.2, which is the first published release containing it. Reconciled by issue #2680; therelease-driftCI gate now blocks a new phantom heading from being introduced.
Changed
BYTEKIT_API_KEY/BYTEKIT_BASE_URLare now the primary environment variables (precedence: flag >BYTEKIT_*> legacyRAPIDCRAWL_*).RAPIDCRAWL_API_KEY/RAPIDCRAWL_BASE_URLcontinue to work as silent fallbacks.--helptext, the missing-key error, and the README now referenceBYTEKIT_*. (#2421)
Removed
- The
recordingscommand (the endpoint is permanently410deactivated). (#2421)
[0.2.0] - 2026-07-15
Additive, lower-risk release — no breaking changes to existing commands.
Added
scrape create --raw/-o, --output <file>: write the raw response body to stdout or a file (#2425).- Local
search --limitvalidation via a commanderargParser, so an invalid limit is rejected before a request is sent (#2425). - One automatic retry on HTTP 429, honoring a clamped
Retry-Afterheader, in the CLI transport (#2425).
Fixed
- Entrypoint guard canonicalized so the
bytekitbin runs correctly when invoked through an npm/npx symlink (#2418).
Changed
- Package renamed to
@hunt-labs/bytekit-cli(the@bytekitnpm scope was unobtainable); thebinname staysbytekit.The priorCorrected in 0.8.1 (see below): that claim does not hold —rapidcrawl-cliname remains installable at its last version but is deprecated.rapidcrawl-cli404s on npm and there is no published version left to deprecate. - Consumer call sites propagate the SDK's narrowed, spec-derived request types (#2426).
- Publish-ready packaging metadata: dropped
main, unpinned the SDK dependency toworkspace:^, addedengines,keywords,homepage,bugs,README.md,LICENSE, and an MITlicensefield.
[0.1.0] - 2026-07-13
Added
- Initial release of the ByteKit CLI, wrapping the ByteKit SDK as shell
subcommands, published under the original
rapidcrawl-cliname.
@hunt-labs/bytekit-mcp
MCP server, published on npm.
[Unreleased]
[0.3.17] - 2026-10-02
Changed
- The bundled documentation snapshot's
sdk/python/clientpage now opens with a runnable install, client,create_scrape.syncand read-the-markdown example before the generated class reference (#5068). Theget_docandsearch_docstools serve the new text; no tool, schema or transport change. - The bundled documentation snapshot's API reference pages now list the keys of nested object
fields as dotted paths under their parent, for example
metadata.titleandmetadata.ogSiteNameon the scrape response, instead of naming only the top-level fields (#5069). Theget_docandsearch_docstools serve the new text; no tool, schema or transport change. - The bundled documentation snapshot's
introductionpage now carries a Python SDK example (AuthenticatedClient,create_scrape.sync,ScrapeRequest) after the TypeScript one in its "Integrate in 60 seconds" block (#5070). Theget_docandsearch_docstools serve the new text; no tool, schema or transport change. - The bundled documentation snapshot's
introductionpage now requests the markdown format in its TypeScript and Python examples and reads the result from the success envelope (result.metadata.title,result.formats.markdown), so a copied snippet shows the response shape (#5072). Theget_docandsearch_docstools serve the new text; no tool, schema or transport change.
[0.3.16] - 2026-10-01
Changed
- The bundled documentation snapshot's bulk API reference pages now list
total_billed_byteson the bulk job bodies (#5021). Theget_docandsearch_docstools serve the new text; no tool, schema or transport change. - The bundled documentation snapshot picks up the Prettier formatting of the docs pages
(#5034), which changes the text served for
billing/bandwidth,guides/errors,guides/monitors,guides/rate-limits,guides/scraping,guides/webhooksandmcp. No tool, schema or transport change. - Refreshed the bundled documentation snapshot after the
billing/balancepage's one-time starter grant for new pay-as-you-go accounts rose from 50 MB to 100 MB of bandwidth, with the 100-credit grant unchanged (#4959). No behavior change in the package: bundled content only, no tool, schema or transport change.
[0.3.15] - 2026-09-30
Fixed
- The docs-bundle builder now reads the
llms.txtindex in its new- [Title](url): descriptionlink shape and ignores URLs outside/docs(the new summary block'sapi.bytekit.comandapp.bytekit.com), so regenerating the bundled documentation snapshot after the docsllms.txtreorder (#5007) still yields the same page set. No tool, schema or transport change.
Changed
- The bundled documentation snapshot now carries the rewritten Introduction page, which states the
base URL, the
BYTEKIT_API_KEYauth header, the install lines for both SDKs, one curl call and one TypeScript SDK call, so an agent that reads only that page can integrate ByteKit (#5005). Theget_docandsearch_docstools serve the new text; no tool, schema or transport change. - The bundled documentation snapshot now carries the SDK package names on the Client Libraries,
TypeScript SDK, Python SDK and Quickstart pages: install
@hunt-labs/bytekit-sdk(npm) /bytekit-sdk(PyPI), and the npm package namedbytekitis not ByteKit's (#5004). Theget_docandsearch_docstools serve the new text; no tool, schema or transport change.
[0.3.14] - 2026-09-29
Changed
- Pinned
@modelcontextprotocol/sdkto the exact version1.31.0instead of the range^1.12.1, so annpx @hunt-labs/bytekit-mcpinstall and the hosted/mcpendpoint run one SDK version instead of thenpxinstall floating to whatever upstream last published (#4949). The pin moves only when we release a deliberate bump. Earlier releases keep the caret range in their published manifest. No tool, schema or transport change. - Refreshed the bundled documentation snapshot after the top-ups page (
billing/top-ups) gained a section on bandwidth owed: an overage that no allowance, USD balance or starter grant can cover is recorded as bandwidth you owe, a top-up, renewal or plan upgrade pays it off first, and new billable requests are refused with402 quota_exceededwhile any is owed (#4960). No behavior change: bundled content only, no tool, schema or transport change.
[0.3.13] - 2026-09-24
Changed
- Refreshed the bundled documentation snapshot after a documentation-wording correction to the
POST /v1/schemabilling description, which now says the page bandwidth is billed at its upstream wire bytes with no endpoint factor instead of "exactly like/v1/scrape" (#4865). No behavior change: bundled content only, no tool, schema or transport change.
[0.3.12] - 2026-09-23
Changed
- Refreshed the bundled documentation snapshot after
GET /v1/account'sAccountSessionResponsegained the required booleanonboarding_completed(#4780). Additive, patch-level: bundled content only, no tool, schema or transport change.
[0.3.11] - 2026-09-21
Changed
- Refreshed the bundled documentation snapshot after the errors guide named
/v1/fetch's422blockedandupstream_errorcodes and thegetfetchandpostfetchpages declared the optionalX-Fetch-IDheader on their422responses (#4732). Bundled content only: no tool, schema or transport change.
[0.3.10] - 2026-09-21
Changed
- Refreshed the bundled documentation snapshot after
GET /v1/fetchandPOST /v1/fetchstarted answering a fetch that exhausted its retries against the target with422(codeblockedorupstream_error) instead of503internal_error(#4669); the bundledgetfetchandpostfetchpages name the new condition. Bundled content only: no tool, schema or transport change.
[0.3.9] - 2026-09-18
Changed
- Refreshed the bundled documentation snapshot after a
GET /v1/bulk/{id}/screenshotsitem started carryingbilling: nullwhile itsstatusis notcompleted(#4582). Bundled content only: no tool, schema or transport change. - Refreshed the bundled documentation snapshot after
POST /v1/schemastarted rejecting slow-path rendering options with error codeunsupported_optioninstead ofunsupported_url(#4584); the bundledcreateschemaextractionpage names the new code. Bundled content only: no tool, schema or transport change. - Dropped the
changelogpage from the bundled documentation snapshot (#4675): the page concatenated four packageCHANGELOG.mdfiles into one entry, making it the single most merge-collided entry in the bundle.search_docs's description no longer describes a bounded window into release history; it now says release history isn't bundled at all and points to the public changelog page instead.
[0.3.8] - 2026-09-17
Changed
- Refreshed the bundled documentation snapshot after the TypeScript SDK changelog entry for
#4563, which records the widened internal
GET /v1/bulkstatus vocabulary (cancelledis now a documented status and an accepted filter). That entry is inside the bundled body of thechangelogpage, and the page's truncation notice reports its new full size. Bundled content only: no tool, schema or transport change.
[0.3.7] - 2026-09-14
Added
scrape_urlaccepts an optionalsolve_challengeboolean (#4450): attempt to clear a Cloudflare challenge; success is not guaranteed. Billed as a normal browser render. The parameter is forwarded toPOST /v1/scrapeonly when set; omitting it sends no key.
Changed
- Refreshed the bundled documentation snapshot after
solve_challengewas published onPOST /v1/scrape: the API reference page for that operation and the TypeScript SDK scrape page list the new option. This refresh itself changes no tool, schema or transport. - The TypeScript SDK changelog entry for #4447 is inside the bundled body of the
changelogpage. - The Python SDK changelog entry for #4448 is past the cut, so a client reading the bundled
changelogpage does not see it. - Refreshed the bundled documentation snapshot after
credits_scrapewas published onGET /v1/usage(#4452): the API reference page for that operation lists the new field. This refresh itself changes no tool, schema or transport. - The TypeScript SDK changelog entry for #4452 is inside the bundled body of the
changelogpage. - The Python SDK changelog entry for #4452 is past the cut, so a client reading the bundled
changelogpage does not see it. - Refreshed the bundled documentation snapshot after
empty_extractionwas documented on thePOST /v1/schemaresponse (#4455). This refresh itself changes no tool, schema or transport. - Refreshed the bundled documentation snapshot after
GET /v1/logsbilling_multiplierwas redocumented as the bandwidth factor derived from the settled bytes, in place of the row's persisted credit count (#4469). This refresh itself changes no tool, schema or transport. - The TypeScript SDK changelog entry for #4469 is inside the bundled body of the
changelogpage. - Refreshed the bundled documentation snapshot after the
POST /v1/schemavalidateddescription recorded the explicitly-nullable exception to the all-null miss (#4490). This refresh itself changes no tool, schema or transport. - The TypeScript SDK changelog entry for #4490 is inside the bundled body of the
changelogpage. - The TypeScript SDK changelog entry for #4455 is inside the bundled body of the
changelogpage. - Refreshed the bundled documentation snapshot after an inline
POST /v1/scrapefailure started carrying a persistedsc_id (#4454): the bundledapi/scrape/createscrapeandsdk/python/scrapepages no longer sayid: null. This refresh itself changes no tool, schema or transport. - The TypeScript SDK changelog entry for #4454 is inside the bundled body of the
changelogpage. - The Python SDK changelog entry for #4454 is past the cut, so a client reading the bundled
changelogpage does not see it. - Refreshed the bundled documentation snapshot after the internal
GET /v1/logsfeed started listing search requests and webhook deliveries (#4460). Only the bundledchangelogpage moved; this refresh itself changes no tool, schema or transport. - The TypeScript SDK changelog entry for #4460 is inside the bundled body of the
changelogpage. - Refreshed the bundled documentation snapshot after the generated
sdk/python/scrapereference stopped listing request fields the OpenAPI spec marksx-internal: true(#4515): that page loses itsx_internal_scrape_pathbullet. The field remains a live internal control in the spec and in the generated model, and the API reference page forPOST /v1/scrapestill lists it. This refresh itself changes no tool, schema or transport. - The MCP server changelog entry for #4515 is past the cut, so a client reading the bundled
changelogpage does not see it; the only other bundled bytes that move are in that page's truncation notice, which reports the full size of the page it truncates. - Refreshed the bundled documentation snapshot after the internal
GET /v1/bulkoperation became cursor-paginated and lost its 100-job cap (#4508): the bundledapi/bulk/listbulkpage now documents the{ data, next_cursor, has_more }page envelope and thelimit/cursorquery parameters in place of a bare array, and both that page's title and theapi/bulkindex card drop the wrong "in-flight" wording — the operation has always returned jobs of every status. This refresh itself changes no tool, schema or transport. - The TypeScript SDK changelog entry for #4508 is inside the bundled body of the
changelogpage.
[0.3.6] - 2026-09-12
Changed
- Refreshed the bundled documentation snapshot after the Python SDK changelog entry for #4340,
which records the split bulk list-item models and the new
BulkCompletedWebhookmodel. Thechangelogpage concatenates four package changelogs and is cut at the 16,384-byte per-page bound; the Python SDK entry is past the cut, so the only bundled bytes that move are in the truncation notice, which reports the FULL size of the page it truncates. Bundled content only: no tool, schema or transport change. - Refreshed the bundled documentation snapshot after the audience-boundary audit of the published docs corpus (#4381). The audit rewrote passages on the billing, scraping, monitors, MCP-tool and command-line pages, and on the generated TypeScript client reference, that described how ByteKit is implemented — internal storage tables and how stored values are written, internal service ownership, operator-only webhook-ingress routes, ByteKit's own staging host, repository provenance and private issue-tracker numbers — rather than what a reader should do or expect. Every public rule, value, API name and error code in those passages is retained, response fields named in the OpenAPI spec included; only the implementation detail is gone. No tool, schema or runtime behavior in this package changed.
[0.3.5] - 2026-09-06
Changed
- Refreshed the bundled documentation snapshot after the TypeScript and Python SDK changelog
entries for #4251, which correct
GET /v1/usage'stopup_balance_bytesdescription to name the one-time starter grant alongside the purchased top-up lots it already named. The only bundled bytes that move are on thechangelogpage. That page concatenates four package changelogs and is cut at the same 16,384-byte per-page bound: the TypeScript SDK entry lands near the top of the page, inside the bundled body, while the Python SDK entry is past the cut and is only reachable at https://bytekit.com/docs/changelog. Bundled content only: no tool, schema or transport change. - Refreshed the bundled documentation snapshot after the TypeScript and Python SDK changelog
entries for #4237, which record that
GET /v1/scrape/{id}reportscache: hitwithout the scrape envelope'scache_age_sfield. The only bundled bytes that move are on thechangelogpage. That page concatenates four package changelogs and is cut at the same 16,384-byte per-page bound: the TypeScript SDK entry lands near the top of the page, inside the bundled body, while the Python SDK entry is past the cut and is only reachable at https://bytekit.com/docs/changelog. Bundled content only: no tool, schema or transport change. - Refreshed the bundled documentation snapshot after the TypeScript and Python SDK changelog
entries for #4203, which record
GET /v1/scrape/{id}as a third delivery path for the scrape envelope'scachefield. The only bundled bytes that move are on thechangelogpage. That page concatenates four package changelogs and is cut at the same 16,384-byte per-page bound: the TypeScript SDK entry lands near the top of the page, inside the bundled body, while the Python SDK entry is past the cut and is only reachable at https://bytekit.com/docs/changelog. Bundled content only: no tool, schema or transport change. - Refreshed the bundled documentation snapshot after the TypeScript SDK's
0.9.4changelog entry for #3984. The only bundled bytes that move are on thechangelogpage. That page concatenates four package changelogs and is cut at the same 16,384-byte per-page bound; the added entry lands near the top of the page, inside the bundled body. Bundled content only: no tool, schema or transport change. - Regenerated the bundled documentation corpus so the offline docs describe monitor
scrape_optionscorrectly (#4024): the description no longer listsheadersamong the accepted fields, sincePOST /v1/monitorsnever honoured a caller-suppliedheadersoption and now rejects it with the same 400 every other capture endpoint returns for the field. No tool, argument or output shape changed. - Refreshed the bundled documentation snapshot after the TypeScript SDK's
0.9.4changelog entry for #4030, which records the third case in whichX-Scrape-Proxy-Tier-Labelreports no tier: a response replayed from stored bytes after this request's own cache lookup missed, which arrives asX-Scrape-Cache: miss. Bundled content only: no tool, schema or transport change. - Refreshed the bundled documentation snapshot after the TypeScript SDK's
0.9.4changelog entry for #4132, which qualifies that third case as an internal deploy-skew safeguard no API request can reach. Bundled content only: no tool, schema or transport change.
[0.3.4] - 2026-09-04
Changed
- Regenerated the bundled documentation corpus so the offline docs describe the proxy-tier
response headers as they now behave (#3926).
X-Scrape-Proxy-Tier-Labelreports the tier that actually served a scrape — after any escalation, never the tier first attempted — and is omitted on a cache hit, which replays stored bytes and runs no tier. No tool, argument or output shape changed.
[0.3.3] - 2026-09-03
Changed
- Breaking (API):
scrape_url'sformatsenum is nowraw | markdown | links | images(#3775). The alias layer that accepted the two retired raw-HTML spellings from an agent and translated them onto the wire is gone with the spellings themselves — there is one vocabulary now, so the tool neither translates nor tolerates. A secondaryrawformat is still rendered under its own--- format: raw ---delimiter; the cleaned-article-HTML format no longer exists.
Fixed
- A
402now reaches the agent with the server's OWN error code and message (#3784). The client carried a hardcoded402branch that rewrote every payment-required response toquota_exceededwith client-authored bandwidth wording, so an impaired or expired subscription — and an overage spending cap — all read as "Bandwidth quota exhausted". The branch is gone; the generic envelope path reports what the gateway sent, so the four billing codes the API documents are the four codes an agent sees.
Changed
-
Refreshed the bundled documentation snapshot for the two subscription-entitlement
402codes now documented in the errors guide (#3784). -
Refreshed the bundled documentation snapshot after the TypeScript and Python SDK changelog entries for #3777, plus this entry itself. The
changelogpage's truncation notice reports the full page size:153564becomes155329bytes (+1765, the combined length of the three new entries). The TypeScript SDK's own changelog section alone already exceeds the page's 16,384-byte bundled cut (the Python SDK, CLI and MCP sections that follow it were never in the bundle, before or after this change), so only the TypeScript SDK's new0.9.2entry is actually visible — it lands near the top of that section, well inside the cut. Both the Python SDK entry and this entry are further down the page than the cut has ever reached. Pushing everything else in the page down by 1765 bytes moves the cut point 1765 bytes earlier within the TypeScript SDK section's own older history, so slightly less of an older entry is visible than before. The page count stays at 108 and no tool, argument, or response shape changes.
[0.3.2] - 2026-08-31
Changed
- Refreshed the bundled documentation snapshot after the CLI's new changelog entry (#3691).
The only bundled byte that moves is the
changelogpage's truncation notice, which reports the full page size:149708becomes151210bytes. That page concatenates four package changelogs and is bound to 16,384 bytes (#3233), so the CLI section itself is still far past the cut and is not in the bundle — the notice's byte count is the whole delta. The page count stays at 108 and no tool, argument, or response shape changes.
[0.3.1] - 2026-08-25
Changed
-
The bundled docs now carry a landing page for every documentation section (#3473).
/docs/billing,/docs/guides,/docs/librariesand/docs/sdkhad no page of their own and returned 404, solist_docsnever offered them andget_doccould not fetch them. Each is now an overview page linking that section's own pages, and the bundle grows from 103 to 107 entries. No tool, argument, or response shape changes. -
Refreshed the bundled documentation snapshot (#3459). The bundled
changelogpage is the docs site's changelog, which concatenates four package changelogs in order —@hunt-labs/bytekit-sdk, the Pythonbytekit-sdk, the CLI, then this package — and every bundled page is bound to 16,384 bytes (#3233), so that page carries the newest TypeScript SDK entries down to the cut and nothing after it. What the refresh actually brings offline is that section's 0.9.1 entry, with the correctedtopup_balance_bytesdescription: the value is the remaining purchased top-up bytes, already net of any overage consumed against them. The Python SDK, CLI and MCP sections are not in the bundle at all — the Python section alone begins roughly 43 KB into the page, far past the bound — so their release notes are not reachable throughget_docorsearch_docs, and full-text search never sees them. Read the whole changelog at https://bytekit.com/docs/changelog. Bundled content only: no tool, schema or transport change.
[0.3.0] - 2026-08-20
Removed
- BREAKING: the bundled docs no longer describe
cookiesorheadersas capture request fields (#3242). Those fields were removed from/v1/scrape,/v1/scrape/bulk,/v1/screenshots,/v1/recordingsand/v1/schema; a request carrying either is rejected with400 validation_error. Tools that composed request bodies from the bundle will no longer see the fields offered.
Changed
get_docnow returns a bounded body, and says when it cut one (#3233). A page above 16,384 bytes comes back cut short, ending in a notice that names the limit, the page's full size, and the docs URL that serves all of it — a caller can never mistake a partial page for a complete one. Every page under the limit is returned byte for byte, in the same{ title, section, body }envelope as before, so nothing else about the tool changes. The cap is advertised in the tool's own description.- The bundled offline docs no longer ship an unbounded page (#3233). The harvest step applies
the same 16,384-byte per-page bound. The generated
changelogpage was 46.5% of the whole bundle on its own; it is still a bundled page, solist_docslists it andsearch_docsranks it, but full-text search now reaches only the releases above the cut. The docs site keeps the whole changelog.
[0.2.8] - 2026-08-19
Changed
- The generated scrape contract's billing factor vocabulary gained
phase1_render(#3141).BillingFactorNameis a closed union copied verbatim intosrc/scrape-contract.generated.ts, and screenshots and recordings bill on a basis the byte-scrape factors cannot name — Phase 1 proxy wire bytes times a 1.5 render-overhead multiplier. A capture's billing node carries that one factor and none of the byte-scrape names; the two vocabularies never mix on a single node. This release is a regeneration of the checked-in contract only, with no behavior change in the server itself: the MCP tools do not call the capture endpoints whose responses now carry the node.
[0.2.7] - 2026-08-18
Changed
- The generated scrape contract now carries the
billingnode (#3139).ScrapeSuccessEnvelopegained an optionalbillingobject withraw_bytes, an orderedfactorslist ofname/value/reasonentries,multiplierandbilled_bytes. The factor list explains exactly how a scrape's charge was composed — endpoint factor, cache-hit discount, clean-markdown surcharge — and multiplies out to theX-Billing-Multiplieron the same response. The existing flatbilling_multiplier/billed_bytespair is unchanged and still emitted beside it; this release is a regeneration of the checked-in contract only, with no behavior change in the server itself.
[0.2.6] - 2026-08-12
Changed
-
scrape_urlnow sends the publicraw_htmlformat spelling on the wire (#2959). It previously translated theraw_htmlan agent asks for into the gateway's internalrawHtmlbefore calling the API. That request only succeeded because the gateway happens to accept both spellings — nothing in the published API contract promised it, so this server depended on an undocumented leniency it could not see. It now sends what the spec documents.No change to what the tool accepts:
raw_htmland the legacyrawHtmlare both still validformatsvalues, and results are rendered identically. The bundledlibraries/typescriptdoc page also gained thesearch,usageandwebhooksresources it had been omitting.
Added
- The published docs bundle now includes the five billing guides linked from the pricing page: balance, bandwidth, credits, spending, and top-ups. The guides describe the source-backed charging, expiry, rollover, and quota behavior. (#2960)
Fixed
- A validation failure from the API now reaches the agent as
validation_error: <message>from every tool, instead of a bareInvalid request body. ({"formErrors":[],"fieldErrors":{…}})with the error code stripped off.scrape_urlandscreenshot_urlare the two tools affected: the client's 422 handling rendered a screenshot's failed-capture body ascode: messagebut passed a validation envelope's message through verbatim, so which spelling an agent saw depended on the HTTP status the endpoint happened to pick — the same vocabulary already renderedvalidation_error: …on a 400. The field details are unchanged, and so areweb_search's render, the concurrency-limit advice, and the 401/402 guidance messages. (#2955) - The hosted
/mcpendpoint now rejects a missing or invalid API key with a JSON-RPC error body ({"jsonrpc":"2.0","error":{"code":-32000,…},"id":null}, HTTP 401) rather than the REST capture envelope, so an MCP host that parses/mcpresponses as JSON-RPC can read the failure it is most likely to hit. Every other transport-level failure on that endpoint already spoke JSON-RPC (405 on GET/DELETE,-32700on a malformed body, 406 onAccept). The ByteKit code (invalid_api_key) is preserved on JSON-RPC'sdatamember, and the capture endpoints' 401 envelope is untouched. (#2955) - The 26 SDK reference pages in the docs bundle (
sdk/python/*,sdk/typescript/*) no longer open with their title and description twice, soget_docandsearch_docsstop spending tokens on a duplicated preamble and search snippets stop reading doubled. The docs site's LLM markdown composer prepended a synthesized# <title>+ description block unconditionally, on top of the generator-authored SDK bodies that already carry their own H1 and intro paragraph; it now yields the title block to a body that leads with an H1. The other 77 bundled pages are byte-identical.sdk/python/client's body heading was also aligned to its page title (# Client Reference→# Client), the one SDK page where the two disagreed. (#2953) - The screenshot render's signed-URL note no longer presents the plan's artifact-retention
date (
expires_at, ~30 days out) as the signed image URL's expiry. The URL is a B2-signed link that dies ~10 minutes after issue (X-Amz-Expires=600) regardless of retention; the note now always states that ~600s signature expiry, so an agent following it no longer stashes a link that reads as good for weeks and is already dead.expires_atitself is untouched — it remains correct for plan retention everywhere else. (#2947) - An unknown flag (e.g.
--bogus-flag) or the space-form--timeout-ms -5(whichnode:util'sparseArgsitself rejects as an ambiguous option value, before it ever reaches the existing--timeout-msvalidation) no longer crashes with a rawnode:internal/util/parse_argsstack trace. Both now render the same one-lineError: ...message the binary already used for--help, a missing API key, and the equals-form/non-numeric--timeout-mscases — parse-layer failures and value-validation failures render through one error path.--help, the missing-key message, and the existing--timeout-ms abc/--timeout-ms=-5errors are unchanged. (#2952)
[0.2.5] - 2026-08-11
Added
- Every outbound request now carries
X-ByteKit-Client: mcp/<package version>, theclient_surfaceattribution marker defined by the Track A analytics contract (#2904), so requests made by the published@hunt-labs/bytekit-mcpserver can be told apart fromapi_directand the other published clients. Set once, unconditionally, at the client's single header-construction site — it does not ride along withAuthorizationand is not affected by the in-process gateway dispatch path (issue #1182). (#2907)
[0.2.4] - 2026-08-10
Changed
- Version-alignment release after the 2026-08-10 publish rescue: the published 0.2.3 artifact was built from the
mainpromotion HEAD, which already contained every change documented under the 0.2.3 heading — but that commit postdates the one that introduced the 0.2.3 version string, so thecheck-release-driftgate (#2680) correctly reported shipped drift on every subsequent CI run (#2829). 0.2.4 realigns manifest ↔ registry ↔ version-introducing commit. No consumer-visible change relative to the published 0.2.3.
[0.2.3] - 2026-08-07
Added
scrape_url/get_resultrender a zero-countLinks (0):/Images (0):header for a format you requested that came back empty, so "the page has none" is no longer indistinguishable from "the format was not returned". A format you did not request still renders nothing at all. (#2687)- The queued-scrape render carries the spec-required
eventsarray, and the success render carriescontent_length— the compressed upstream wire-byte count that is the billing basis for the call, which nothing else in the output surfaced. (#2687) - Screenshot renders carry
page_title(a spec field the tool description already promised as "metadata") and a note thatimage_urlis a signed link with a short lifetime, using the concreteexpires_atinstant when the API supplies one. (#2687) - A queued scrape now gets the same "Use
get_resultwith this ID to check when it completes." guidance a pending screenshot got in 0.2.2. (#2687) screenshot_urlexposesquality,dark_mode,countryandlanguage. None of them can reach the queued outcome;qualityanddark_modeare additionally cost-neutral (a screenshot bills on the phase-1 fetch's wire bytes, never the artifact size), whilecountry/languagechange which page the origin serves and so can change that figure.token_budget,clean_markdownandcache_ttlstay unexposed onscrape_url, each for a reason recorded insrc/tools/scrape-url.ts. (#2687)- New output contract.
scrape_url/get_resultrender the envelope'swarningsunder aWarnings (N):section, onecode: message (element)line per warning, on both the success and the terminal-failure render. The(element)suffix is omitted for the whole-extraction codes that carry no element. Nothing rendered them before, so anartifact_unavailable— a format you requested and were billed for is missing from the response — was indistinguishable from a page that simply had none of it. The block is budgeted inside the same 100KB cap as the content, ahead of the links/images sections, so a long links list cannot crowd a failure signal out. Awarningsarray that is empty or absent renders nothing, exactly as before. (#2679)
Fixed
- Compile-time breaking for TypeScript consumers that read
SearchResult.snippetorSearchResult.dateunguarded. Both were declared required on the exportedSearchResulttype, butPOST /v1/searchomits both keys fortype: 'images'results —docs/api/openapi.yamldeclaresrequired: [position, title, url]and nothing else for a search result. Readingresult.snippet.lengththerefore compiled under strict TypeScript and threw at runtime on any images result. They are nowsnippet?: stringanddate?: string | null. Runtime behaviour is unchanged — the server has always passed the API's JSON through verbatim, and no MCP tool output moved; what changes is thattscnow requires a guard (result.snippet?.length, or anif (result.snippet !== undefined)narrowing) at every read site.web/newsresults still carry both fields anddateis still nullable there, sonull(the provider reported no date) stays distinguishable from absent (the index carries no such field at all). (#2781) - The hosted
/mcptransport and a localnpxinstall now render invalid-argument errors identically. They had diverged — hosted returned the raw Zod issue-array JSON where stdio returned the flattenedRequired at url— and the cause was neither a stale deploy nor a difference in ByteKit source: both transports share one server factory, and the text is produced inside@modelcontextprotocol/sdk. What differed was the SDK version each side resolved.@modelcontextprotocol/sdk1.30.0 rewrotegetParseErrorMessageto format Zod issues as<message> at <path>, where 1.29.0 fell through toZodError.message(the raw JSON array). The hosted image builds with a frozen lockfile, which still pinned 1.29.0, while the unpinned^1.12.1range let annpxconsumer resolve 1.30.0. The lockfile is now bumped to 1.30.0 so both sides run the same SDK; the declared range is deliberately left at^1.12.1so upstream fixes keep arriving. (#2682) - A truncated links or images list is cut between whole entries instead of mid-URL, and
each affected section is annotated with
[Links truncated: N of M entries shown], so the section header's count stays reconcilable with what was actually returned. The markers are spent from the same 100KB budget, not appended on top of it. (#2687) web_searchno longer prints the API error code twice (validation_error: … (validation_error)); the code is appended only when the message does not already carry it. (#2687)scrape_urlrejectsformats: []at the schema boundary — matching the API's ownminItems: 1— instead of forwarding it and returning a server 422. Omitting the parameter still yields the["markdown"]default. (#2687)--timeout-mswith a non-numeric value is rejected with an error instead of being silently ignored, which had left the client running on its default budget while the operator believed one was set. (#2687)- Reading
bytekit://accountissues its two sub-requests serially, matching theget_accounttool — concurrent sub-requests could rate-limit a 1-rps plan against itself. (#2687) list_docswith asectionoutside the documentation taxonomy returns an error naming the valid set, rather than a silent empty list that a correctly-spelled but empty section would also produce. (#2687)- The Claude Desktop /
mcp-remotebridge config in README.md now authenticates as written. It previously passed"Authorization: Bearer $BYTEKIT_API_KEY"inargs; an MCP host launchescommand/argswith no shell, andmcp-remoteinterpolates only the braced${VAR}form, so the literal text was sent as the bearer token and the endpoint answered401. The snippet now uses"Authorization:${AUTH_HEADER}"with a matchingenvblock, and the hosted-HTTP snippet carries the same no-shell-expansion caveat. (#2688) - The bundled documentation no longer teaches the deactivated
/v1/recordingsendpoint or links its removed reference pages. (#2688) - A 429 caused by the account's concurrency cap now says so and fails fast, instead of
being reported as a rate limit. HTTP 429 carries two conditions (
rate_limitedandconcurrency_limit), and the client read neither the body nor the code: every 429 got oneRetry-Afterretry and thenRate limited. Try again in a moment.with the error code hardcoded torate_limited. The concurrency variant does not clear on that back-off — the API spec calls itsRetry-After"a fixed small back-off" while the slots free only when the in-flight jobs finish — so the retry was spent on a condition a second cannot clear, and the error object named the wrong cause. Aconcurrency_limit429 now throws immediately with the real wait scale anderrorCode: 'concurrency_limit', at both 429 sites.rate_limited429s, and any 429 whose body is empty, non-JSON or carries no recognizable code, keep the single honored-header retry and the exact message and code they had. (#2681) - A screenshot that fails inside the API's sync hold now surfaces its REAL failure code
(
blocked,timeout, …) and message.POST /v1/screenshotsanswers that case with HTTP 422 carrying the flat screenshot resource, not the unified error envelope, and the client parsed neither — so every such failure read as the literalValidation error.. The(Screenshot ID: ss_…)handle line is unchanged, and a genuine request-validation 422 still renders exactly as it did. (#2679)
[0.2.2] - 2026-07-29
Fixed
scrape_url/get_resultrender every returned textual format (markdown/html/raw_html), not just the first present — a multi-format call previously dropped every secondary format silently despite it being billed. Secondary formats are rendered under a--- format: <name> ---delimiter, in canonical order after the unlabeled primary. (#2589)- The 100KB output truncation budget now covers the links/images blocks too — they were previously appended AFTER truncation, so a combined output could exceed the budget. The whole assembled payload is now truncated exactly once, with the links/images block reserved its own share of the budget so it is never dropped in full just because the textual formats alone already fill the budget. (#2589)
scrape_url/get_resultdescriptions no longer advertise a queuedsc_outcome that the MCP input schema (url/formats/country) cannot produce — that outcome was only reachable viabody.asyncor fieldscan-fast-path.tsgates on, none of which this tool's schema exposes. (#2589)web_searchnow renderscredits_usedandrelated_searches, both spec-required fields that were previously dropped silently. (#2591)- A sync-failed screenshot's
ss_handle — absent from the 422 error body — is now read from theX-Screenshot-IDresponse header and included in the error text, so it can still be redeemed viaget_result. Degrades gracefully when the header is absent. (#2591) - The 402 message on
/v1/screenshotsno longer says "Bandwidth quota exhausted" and no longer points atget_account, which is rate-limit-fragile immediately after a quota failure. Every other endpoint's 402 wording is unchanged. (#2591) (Correction, #3785: this entry originally explained the change by saying screenshot quotas are credit/count-based. They are not. Screenshots are billed on bandwidth — Phase 1 wire bytes x 1.5 — and the credit figure they report is a legacy weight.) get_result's invalid-id-prefix message no longer truncates the offending id to 3 characters. (#2591)get_resultnow gives the same "poll with get_result" guidancescreenshot_url's queued branch gives when a screenshot is stillpending/processing. (#2591)get_accountno longer renders a duplicate email (Account: [email protected] ([email protected])) when the account has no name. (#2591)search_docssnippets no longer duplicate the page title when the match falls near the body's own leading# <Title>heading. (#2591)ScrapeErrorEnvelope.idis now modeled as nullable, matchingdocs/api/openapi.yaml(idis null when a scrape was never queued, e.g. a validation failure) —get_result/scrape_urlrenderID: —instead of the literalID: null. (#2591)
Changed
get_accountnow issues itsgetAccount/getUsagerequests serially instead of viaPromise.all, so it no longer doubles the concurrent request rate against 1-rps plans. (#2591)- Regenerated the bundled
src/docs-bundle/docs-bundle.jsonagainst the currentpackages/docs/contenttree. Every guarded endpoint's docs page now states its request-body byte ceiling and therequest_too_largeerror code (#2640), and the/v1/bulkpages documentdefaults.typeinheritance plus the 422 on unrecognized keys (#2643). Content only — no tool surface, schema, or runtime behavior changed. (#2640, #2643)
[0.2.1] - 2026-07-24
Fixed
- The spec'd flat
/v1/search502 error body ({"error":"search_provider_error"}) now surfaces its code in the tool error text instead of collapsing to a generic "ByteKit API error (HTTP 502)". Flat{error: "<string>"}bodies on any status parse the same way; envelope-shaped bodies keep winning when both shapes are present. (#2541)
Added
BytekitClientaccepts atimeoutMsconstructor option — a per-request budget in milliseconds, defaulting to120000. Set0to disable it entirely. Values that are negative or non-finite normalize to disabled rather than throwing. Previously a hung upstream hung a stdio MCP tool call forever. (#2568)- The budget covers an entire
request()call rather than each await separately: the initial fetch, the 429Retry-Aftersleep, the retry fetch, and both the success and error body reads (response.json()inrequest()and inparseErrorEnvelope). Two of those previously swallowed an abort outright — theRetry-Aftersleep observed no signal, and a failed error-body read was downgraded to "no envelope", surfacing a misleading status-line message instead of the timeout. Exhausting the budget now produces aBytekitApiErrorwitherrorCode: 'timeout', distinct from'network_error'. (#2568) - Note the semantics are client-side only: aborting does not cancel or refund
server-side work already in progress, so an account may still be billed for a result
the caller never reads. The
120000default is deliberately well above the server-side screenshot ceiling so it never truncates a legitimate billable operation. (#2568)
[0.2.0] - 2026-07-21
Removed (BREAKING)
- The legacy
RAPIDCRAWL_API_KEYenvironment-variable fallback (kept as a one-minor-version bridge in #2421) is removed. UseBYTEKIT_API_KEY(or the--api-keyflag). (#2473)
Fixed
- Tool-surface drift against the live API (#2477):
screenshot_url'sdeviceenum is nowdesktop/mobile—tabletwas 422-rejected by the API.- Screenshot results render the spec field names (
image_width,image_height,file_size_bytes); the removedformatfield is gone. get_resultrenders a failed job's envelope (Status: failed+error.code/error.message) instead ofundefinedlines, and omitsCredits: nullfor pending rows.get_accountno longer rendersAccount: nullwhen the account has no name.get_doc's description example uses a real bundle key.
- Server errors now surface diagnostics instead of a generic failure: any
non-2xx with a parseable API envelope renders its
code+message(e.g. 503blocked, 400invalid_url), 422 includeserror.details, andRetry-After(including HTTP-date or garbage values) is parsed to a bounded retry delay instead ofsetTimeout(NaN). Specific 401/402/429 mappings still win. (#2478)
Changed
- The bundled offline docs (
get_doc/list_docs/search_docs) are regenerated: 96 pages, now including the TypeScript SDKusageandwebhooksreference pages and dropping removed content. (#2469)
[0.1.3] - 2026-07-16
Changed (BREAKING)
- The MCP tool
web-searchis renamed toweb_search(snake_case, consistent with every other tool). MCP clients bound to the oldweb-searchname must update. (#2421)
Changed
- The 401 error message now references
BYTEKIT_API_KEY;--base-urlnow falls back to theBYTEKIT_BASE_URLenvironment variable when no flag is passed. (#2421)
Fixed
- The bundled offline docs (
get_doc/list_docs/search_docs) now teach the correct@hunt-labs/bytekit-sdkinstall/import name instead of the dead@bytekit/sdkscope. (#2449)
[0.1.2] - 2026-07-15
Fixed
scrape_urlnow reads the snake_case envelope fields the API actually returns (id,final_url,status_code); the published 0.1.1 tarball read the old camelCase fields (scrapeId,finalUrl,statusCode), so every metadata line renderedundefined(#2419).- The MCP handshake
serverInfo.versionis now derived frompackage.jsoninstead of a hardcoded literal, so it can never drift from the published version (#2419).
Changed
- Compiled test code and fixtures are excluded from the published tarball — only
runtime
dist/**(plusREADME.md,LICENSE, and nowCHANGELOG.md) ships (#2419).
[0.1.1] - 2026-07 [DEPRECATED]
Note
- Superseded by 0.1.2. This release shipped stale
scrape_urlmetadata rendering (camelCase fields the API no longer returned) and included compiled test code in the tarball. Upgrade to 0.1.2 or later.
[0.1.0] - 2026-07
Added
- Initial release of the ByteKit MCP server, exposing ByteKit web-data tools (scrape, screenshot, search, docs) to AI agents over the Model Context Protocol, as a local stdio server or hosted.