{"_id":"@aishorty/sdk","_rev":"2-e45d6dfc476e507928fa525b1d3cabb1","name":"@aishorty/sdk","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@aishorty/sdk","version":"0.1.0","keywords":["shorty","aishorty","transcription","summaries","subtitles","api","sdk"],"author":{"name":"Devino Solutions"},"license":"MIT","_id":"@aishorty/sdk@0.1.0","maintainers":[{"name":"mraladdin","email":"aladdin@devino.ca"}],"homepage":"https://aishorty.com/developers","bugs":{"url":"https://github.com/DevinoSolutions/shorty-sdk/issues"},"dist":{"shasum":"bf7a06490bd918f9ae62b8b7f454f3b3a1b3e87e","tarball":"https://registry.npmjs.org/@aishorty/sdk/-/sdk-0.1.0.tgz","fileCount":9,"integrity":"sha512-uS0pmWGed5QEcTeo24e0BFfh568mk40OFARopQa3MS2fAnhPWNVdCMxHuJ+/sEdYytPY/nxmuY64uvBV7EepUg==","signatures":[{"sig":"MEYCIQDXSKvPOfDO8ickygJVSJjbUxw4eGcfE3YwoAZ2jw7skQIhAIM8OW/e1ouCw+01/aMWHm2sgjlgJgPwBAve/NRq+EzI","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":263626},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=20"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"gitHead":"c07ca03b5970227ba1b0e13878053259962bbabd","scripts":{"test":"tsx --test \"test/**/*.test.ts\"","build":"tsup","release":"changeset publish","version":"changeset version","changeset":"changeset","test:unit":"tsx --test \"test/**/*.test.ts\"","typecheck":"tsc --noEmit","prepublishOnly":"pnpm run typecheck && pnpm run test && pnpm run build"},"_npmUser":{"name":"mraladdin","email":"aladdin@devino.ca"},"repository":{"url":"git+https://github.com/DevinoSolutions/shorty-sdk.git","type":"git"},"_npmVersion":"11.13.0","description":"Official TypeScript SDK for the Shorty public API (aishorty.com/v1).","directories":{},"sideEffects":false,"_nodeVersion":"24.16.0","_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.23.1","tsup":"^8.5.1","typescript":"^5.8.3","@types/node":"^20.12.11","@changesets/cli":"^2.31.1"},"_npmOperationalInternal":{"tmp":"tmp/sdk_0.1.0_1785332480823_0.9297403888153406","host":"s3://npm-registry-packages-npm-production"}}},"time":{"created":"2026-07-29T13:41:20.615Z","modified":"2026-08-05T02:30:03.077Z","0.1.0":"2026-07-29T13:41:20.979Z"},"bugs":{"url":"https://github.com/DevinoSolutions/shorty-sdk/issues"},"author":{"name":"Devino Solutions"},"license":"MIT","homepage":"https://aishorty.com/developers","keywords":["shorty","aishorty","transcription","summaries","subtitles","api","sdk"],"repository":{"url":"git+https://github.com/DevinoSolutions/shorty-sdk.git","type":"git"},"description":"Official TypeScript SDK for the Shorty public API (aishorty.com/v1).","maintainers":[{"email":"aladdin@devino.ca","name":"mraladdin"},{"email":"amin@devino.ca","name":"devino-solutions"}],"readme":"# @aishorty/sdk\n\nThe official TypeScript SDK for the [Shorty](https://aishorty.com) public API\n(`https://aishorty.com/v1`). Transcribe media, summarize videos and web pages,\ngenerate subtitles, and poll async jobs — with typed errors, automatic retries,\ncursor pagination, and Standard-Webhooks signature verification.\n\n- **Zero runtime dependencies.** Native `fetch`, `node:crypto`, `AbortSignal`.\n- **Node.js >= 20.** ESM and CommonJS builds, full type declarations.\n- **Server-side only** — your API key is a secret. The client throws if it\n  detects a browser environment.\n\n## Install\n\n```bash\nnpm install @aishorty/sdk\n# or: pnpm add @aishorty/sdk  /  yarn add @aishorty/sdk\n```\n\n## Quickstart\n\n```ts\nimport { Shorty } from '@aishorty/sdk'\n\nconst shorty = new Shorty({ apiKey: process.env.SHORTY_API_KEY })\n\n// Start a summary job for a YouTube video…\nconst job = await shorty.summaries.create({\n    source: 'youtube',\n    url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',\n})\n\n// …then wait for it to finish.\nconst finished = await shorty.jobs.waitFor(job.job_id)\nconsole.log(finished.status, finished.output)\n```\n\n## Authentication\n\nShorty accepts exactly one auth form: a **Bearer API key** (`shk_live_…`), sent\non every request as `Authorization: Bearer <key>`. Create keys in your developer\nconsole.\n\n```ts\n// Explicit:\nconst shorty = new Shorty({ apiKey: 'shk_live_…' })\n\n// Or omit it and the SDK reads process.env.SHORTY_API_KEY:\nconst shorty = new Shorty()\n```\n\nThe key is held in a closure — it never appears in `JSON.stringify(client)`,\n`util.inspect(client)`, thrown errors, or debug logs.\n\n### Client options\n\n| Option | Default | Description |\n| --- | --- | --- |\n| `apiKey` | `process.env.SHORTY_API_KEY` | Your `shk_live_…` key. Throws if unset. |\n| `baseUrl` | `https://aishorty.com` | API origin (paths already include `/v1`). |\n| `timeoutMs` | `60000` | Per-attempt timeout. |\n| `maxRetries` | `2` | Retries after the first attempt. |\n| `fetch` | `globalThis.fetch` | Override the fetch implementation. |\n| `debug` | `false` | `true` logs to stderr; a function receives redacted log lines. |\n| `dangerouslyAllowBrowser` | `false` | Allow construction where `window` exists (not recommended). |\n\n## Resources\n\n```ts\nawait shorty.usage.get()\n\nconst page = await shorty.articles.list({ limit: 20 })\nawait shorty.articles.search({ q: 'transformers', article_type: 'YOUTUBE_ARTICLE' })\nawait shorty.articles.get(articleId)\n\nawait shorty.transcriptions.list()\nawait shorty.transcriptions.get(id)\nawait shorty.transcriptions.create({ url: 'https://example.com/audio.mp3' })\n\nawait shorty.summaries.create({ source: 'url', url: 'https://example.com/post' })\nawait shorty.summaries.create({ source: 'text', content: 'Long text to summarize…' })\n\nawait shorty.subtitles.create({ url: 'https://example.com/clip.mp4', style: 'TIKTOK' })\nawait shorty.subtitles.download(jobId, { kind: 'srt' })\n\nawait shorty.jobs.get(jobId)\nawait shorty.jobs.waitFor(jobId)\n```\n\nEvery method takes a trailing `opts?` with `{ signal, timeoutMs, headers }`\n(and `idempotencyKey` on the create methods).\n\n## Pagination\n\n`list()` returns a `Page`. Iterate it with `for await` to walk **every** item\nacross all pages (the SDK follows the opaque cursor for you, reusing your\noriginal filters), or page manually.\n\n```ts\n// Auto-iterate all items:\nfor await (const article of await shorty.articles.list({ limit: 50 })) {\n    console.log(article.id, article.title)\n}\n\n// Manual page control:\nlet page = await shorty.articles.list({ limit: 50 })\nwhile (page) {\n    console.log(page.data.length, 'items; hasMore =', page.hasMore)\n    page = (await page.nextPage()) ?? null\n    if (!page) break\n}\n```\n\n> Cursors are **filter-bound**: a cursor minted under one query cannot be\n> replayed against a different filter. The SDK never mutates your params while\n> paging, so this is handled automatically — just don't hand a cursor to a\n> different `list()` call.\n\n## Jobs & `waitFor`\n\nWrites (`transcriptions.create`, `summaries.create`, `subtitles.create`) return\na `202` `{ job_id, status, tracking_url }`. Poll with `jobs.get(id)`, or use the\nconvenience poller:\n\n```ts\ntry {\n    const done = await shorty.jobs.waitFor(job.job_id, {\n        pollIntervalMs: 2000, // default\n        timeoutMs: 600_000, // default\n        // signal: abortController.signal,\n    })\n    console.log('done:', done.output)\n} catch (err) {\n    if (err instanceof JobFailedError) {\n        // The job ended in ERROR/CANCELLED — err.jobError has the reason.\n    }\n    if (err instanceof APITimeoutError) {\n        // The deadline elapsed before the job finished.\n    }\n}\n```\n\n`waitFor` is a **client-side loop** — not a server feature. It resolves on\n`SUCCESS`, throws `JobFailedError` on `ERROR`/`CANCELLED`, and `APITimeoutError`\non the deadline.\n\n## Errors\n\nEvery non-2xx response throws a typed error. The class is chosen by the problem\n`code` first, falling back to the HTTP status class for any unknown/absent code\n(so a new server code never breaks your `instanceof` handling).\n\n```ts\nimport {\n    APIError,\n    AuthenticationError,\n    PermissionDeniedError,\n    NotFoundError,\n    ConflictError,\n    ValidationError,\n    RateLimitError,\n    QuotaExhaustedError,\n    APIServerError,\n    APIConnectionError,\n    APITimeoutError,\n} from '@aishorty/sdk'\n\ntry {\n    await shorty.usage.get()\n} catch (err) {\n    if (err instanceof QuotaExhaustedError) {\n        // Period allowance spent — back off until the next window.\n    } else if (err instanceof RateLimitError) {\n        console.log('retry after', err.retryAfterSeconds, 's')\n    } else if (err instanceof APIError) {\n        console.log(err.status, err.code, err.title, err.detail, err.requestId)\n        console.log(err.errors) // field-level validation errors, when present\n    }\n}\n```\n\nEvery `APIError` carries `status`, `code`, `problemType`, `title`, `detail`,\n`requestId`, `errors?`, `headers`, and (on 429s) `retryAfterSeconds`. The\n`.message` is `title: detail (request_id)` and never contains your API key.\n\n### Error code reference\n\n| Code | Status | SDK error class |\n| --- | --- | --- |\n| `unauthorized` | 401 | `AuthenticationError` |\n| `invalid_api_key` | 401 | `AuthenticationError` |\n| `insufficient_scope` | 403 | `PermissionDeniedError` |\n| `feature_not_enabled` | 403 | `PermissionDeniedError` |\n| `resource_not_found` | 404 | `NotFoundError` |\n| `idempotency_conflict` | 409 | `ConflictError` |\n| `idempotency_in_progress` | 409 | `ConflictError` |\n| `resource_not_ready` | 409 | `ConflictError` |\n| `validation_failed` | 400 | `ValidationError` |\n| `idempotency_key_reused` | 422 | `ValidationError` |\n| `request_too_large` | 413 | `ValidationError` |\n| `rate_limited` | 429 | `RateLimitError` |\n| `quota_exhausted` | 429 | `QuotaExhaustedError` |\n| `internal_error` | 500 | `APIServerError` |\n| `service_unavailable` | 503 | `APIServerError` |\n\nTransport-level failures throw `APIConnectionError` (with `cause`) or\n`APITimeoutError`.\n\n## Retries & idempotency\n\nThe SDK automatically retries transient failures up to `maxRetries` times\n(default 2) after the first attempt.\n\n**Retried:** connection errors · per-attempt timeouts · `408` · `429`\n`rate_limited` (or a `429` with no parseable code) · `500` / `502` / `503` /\n`504`.\n\n**Never retried:** `429` `quota_exhausted` (a spent period allowance — retrying\nwon't help) · any other `4xx` · requests you aborted via your own `signal`.\n\n**Backoff:** a `Retry-After` header is honored when present (both delta-seconds\nand HTTP-date forms, capped at 60s); otherwise full-jitter exponential backoff,\n`random(0, min(0.5 · 2^attempt, 8))` seconds.\n\n**POST safety:** an unsafe `POST` is retried **only** when it carries an\n`Idempotency-Key`. The three create methods auto-generate one (a UUID) and reuse\nthe **same** key across every retry attempt, so retries are deduplicated\nserver-side. Supply your own via `opts.idempotencyKey` to make a call idempotent\nacross process restarts:\n\n```ts\nawait shorty.summaries.create(\n    { source: 'text', content: '…' },\n    { idempotencyKey: 'my-stable-key-123' },\n)\n```\n\n`client.request(...)` POSTs are **not** retried unless you pass an\n`idempotencyKey` yourself.\n\n## Webhooks\n\nVerify inbound Shorty webhooks with `verifyWebhookSignature`. It implements the\n[Standard Webhooks](https://www.standardwebhooks.com) v1 scheme (HMAC-SHA256,\n`webhook-id` / `webhook-timestamp` / `webhook-signature` headers, rotation via\nmultiple space-separated signatures, a 5-minute replay tolerance, constant-time\ncomparison).\n\n```ts\nimport { verifyWebhookSignature } from '@aishorty/sdk'\n\n// Express example — note express.raw(), NOT express.json():\napp.post('/webhooks/shorty', express.raw({ type: '*/*' }), (req, res) => {\n    const result = verifyWebhookSignature({\n        payload: req.body, // the RAW body (Buffer/Uint8Array or string)\n        headers: req.headers, // Headers object or lowercase-keyed record\n        secret: process.env.SHORTY_WEBHOOK_SECRET!, // whsec_…\n    })\n    if (!result.valid) {\n        return res.status(400).json({ error: result.reason })\n    }\n    // Safe to handle. Parse the body only AFTER verifying.\n    const event = JSON.parse(Buffer.from(req.body).toString('utf8'))\n    res.sendStatus(200)\n})\n```\n\n> **Raw body only.** You must verify the **exact bytes** you received. Do not\n> `JSON.parse` and re-`JSON.stringify` the body before verifying —\n> re-serialization reorders keys and changes whitespace, which **will** fail\n> verification. In frameworks that auto-parse JSON, capture the raw body first\n> (e.g. `express.raw()`, or `await request.text()` in a Next.js route).\n\n`verifyWebhookSignature` returns a discriminated result:\n\n```ts\ntype WebhookVerifyResult =\n    | { valid: true }\n    | {\n          valid: false\n          reason:\n              | 'missing_headers'\n              | 'malformed_timestamp'\n              | 'timestamp_out_of_tolerance'\n              | 'no_matching_signature'\n      }\n```\n\n## License\n\nMIT © Devino Solutions\n","readmeFilename":"README.md"}