---
title: "TypeScript SDK"
description: "Convert spreadsheets, CSV and JSON into SQL from TypeScript or JavaScript with @intosql/sdk - upload, wait, download, webhooks and errors."
canonical: https://intosql.com/docs/sdks/typescript
last_updated: 2026-10-15
---

# TypeScript SDK

`@intosql/sdk` wraps the [IntoSQL API](/docs/api) for TypeScript and JavaScript, one of the [official SDKs](/docs/sdks). One call uploads a file, starts the conversion and waits for the result. The package has no dependencies and uses the platform's `fetch` and Web Crypto.

> **Pro:** The API is part of IntoSQL Pro. Make an API key in [Settings, API keys](https://intosql.com/settings/api-keys) and keep it out of your source code.

## Install

```shell
npm install @intosql/sdk
```

The package is ESM only and runs on Node 20 or newer, Deno, Bun and edge runtimes.

## Create a client

```ts
import { IntoSQL } from '@intosql/sdk';

const intosql = new IntoSQL({ apiKey: process.env.INTOSQL_API_KEY! });
```

The constructor throws if `apiKey` is an empty string.

| Option       | Type           | Default                      | What it does                                                                                             |
| ------------ | -------------- | ---------------------------- | -------------------------------------------------------------------------------------------------------- |
| `apiKey`     | `string`       | required                     | An API key from Settings, API keys. Sent as `Authorization: Bearer <key>`.                               |
| `baseUrl`    | `string`       | `https://intosql.com/api/v1` | The API's base address. A trailing slash is removed.                                                     |
| `maxRetries` | `number`       | `2`                          | Attempts after the first one for rate limits, server errors and lost connections. `0` turns retries off. |
| `fetch`      | `typeof fetch` | `globalThis.fetch`           | The `fetch` the client uses for every call, the upload included. Useful for proxies and tests.           |

The client never sends your key to another origin. If a pagination link points somewhere other than `baseUrl`'s origin, the call throws instead.

## Convert a file

`convert(file, options)` does the whole flow and resolves with the finished conversion.

```ts
import { openAsBlob } from 'node:fs';
import { IntoSQL, spec } from '@intosql/sdk';

const intosql = new IntoSQL({ apiKey: process.env.INTOSQL_API_KEY! });

const conversion = await intosql.convert(await openAsBlob('ventas.xlsx'), {
    name: 'ventas.xlsx',
    spec: spec({ dialect: 'postgresql', table: 'ventas' }),
});

console.log(conversion.report?.rows.converted, conversion.output?.url);
```

What happens, in order:

1. **Open.** `POST /conversions` with `name`, the file's size in bytes and `spec`. The answer has an upload address.
2. **Upload.** The file is sent with a `PUT` to that address, with the headers the API gave, as it is. Nothing is read into memory first.
3. **Start.** `POST /conversions/{id}/start`.
4. **Wait.** The conversion is read until its status is `done` or `failed`.

`file` is an `Upload`: a `Blob` or `File`, a `Uint8Array`, or `{ stream, bytes }` for a `ReadableStream<Uint8Array>` with its size in bytes. The size must be exact, since the conversion only starts once the whole file has arrived.

```ts
const response = await fetch('https://example.com/exports/pedidos.json');

const conversion = await intosql.convert(
    {
        stream: response.body!,
        bytes: Number(response.headers.get('Content-Length')),
    },
    { name: 'pedidos.json', spec: spec({ dialect: 'sqlite' }) },
);
```

`name` helps tell the file's format, and the downloaded result is named after it: `ventas.xlsx` downloads as `ventas.sql`. Set `table` in the spec to choose the table's name.

### How it waits

`convert`, `convertUrl` and `conversions.wait` read the conversion again and again with growing pauses: half a second, then 1, 2 and 4 seconds, then every 5 seconds. They resolve when the status is `done`, throw `ConversionFailedError` when it is `failed`, and `ConversionCanceledError` when it was canceled.

| Wait option | Type          | Default         | What it does                                                                                                  |
| ----------- | ------------- | --------------- | ------------------------------------------------------------------------------------------------------------- |
| `timeoutMs` | `number`      | `3600000` (1 h) | Gives up after this many milliseconds and throws an `Error` saying the conversion is still queued or running. |
| `signal`    | `AbortSignal` | none            | Stops waiting when aborted. The abort reason is thrown.                                                       |

Giving up only stops the waiting. The conversion keeps running on IntoSQL's servers, and you can read it later by its `id`.

```ts
const conversion = await intosql.convert(
    await openAsBlob('clientes_2024.xlsx'),
    {
        name: 'clientes_2024.xlsx',
        spec: spec({ dialect: 'mysql' }),
        timeoutMs: 10 * 60 * 1000,
        signal: AbortSignal.timeout(15 * 60 * 1000),
    },
);
```

### Convert a database dump

A MySQL or MariaDB dump (`.sql`, `.sql.gz`) converts to PostgreSQL like any other file. The report then lists every table with its own rows, columns, fixes and problems, plus `source` and `not_converted`.

```ts
for (const table of conversion.report?.tables ?? []) {
    console.log(table.name, table.rows, table.problems.length);
}
```

## Convert a file by URL

`convertUrl(url, options)` asks IntoSQL to download the file from a public HTTPS address, then waits the same way. There is no upload step.

```ts
const conversion = await intosql.convertUrl(
    'https://example.com/exports/ventas.csv',
    { spec: spec({ dialect: 'mysql' }) },
);
```

`options` takes `spec`, an optional `name` (otherwise the name comes from the server's `Content-Disposition` header or the address), and the wait options. The address must be HTTPS and reachable from the internet; anything else is refused with `invalid_url`. If the download fails, the conversion ends as `failed`.

## Download the result

When `status` is `done`, `output.url` downloads the result. The address works for ten minutes. After that, read the conversion again to get a fresh one.

```ts
import { createWriteStream } from 'node:fs';
import { Readable } from 'node:stream';
import { pipeline } from 'node:stream/promises';

const fresh = await intosql.conversions.retrieve(conversion.id);
const download = await fetch(fresh.output!.url);

await pipeline(
    Readable.fromWeb(download.body!),
    createWriteStream('ventas.sql'),
);
```

Send no `Authorization` header to `output.url`; the address carries its own signature. Files are deleted 24 hours after the conversion finishes, or right away with `conversions.delete`.

## Build a spec

`spec(options)` returns a full `JobSpec` with IntoSQL's defaults. Only `dialect` is required.

```ts
import { spec } from '@intosql/sdk';

const request = spec({
    dialect: 'sql_server',
    table: 'clientes',
    schema: 'ventas',
    sheet: 'Clientes 2024',
    batchRows: 1000,
});
```

| Option        | Type                                                                           | Default              | Spec field            |
| ------------- | ------------------------------------------------------------------------------ | -------------------- | --------------------- |
| `dialect`     | `'postgresql' \| 'mysql' \| 'mariadb' \| 'sqlite' \| 'sql_server' \| 'oracle'` | required             | `output.dialect`      |
| `format`      | `'sql' \| 'csv' \| 'json'`                                                     | `'sql'`              | `output.format`       |
| `table`       | `string \| null`                                                               | chosen by the engine | `output.table`        |
| `schema`      | `string \| null`                                                               | none                 | `output.schema`       |
| `sheet`       | `string \| null`                                                               | first sheet          | `input.sheet`         |
| `createTable` | `boolean`                                                                      | `true`               | `output.create_table` |
| `batchRows`   | `number`                                                                       | `500`                | `output.batch_rows`   |

The rest is fixed by the helper: format, encoding, delimiter and header row detected from the file; column names in `snake_case`; `INSERT` statements (`style: 'inserts'`); types inferred from the first 10,000 rows; date order detected; leading zeros kept; no per-column overrides. For anything else (`bulk` output, a date order, a locale, columns renamed, kept as text or left out), change the object it returns or write a `JobSpec` yourself. Every field is in [Conversion spec](/docs/api/spec).

```ts
const request = spec({ dialect: 'postgresql', table: 'pedidos' });

request.output.style = 'bulk';
request.inference.date_order = 'day_first';
request.columns = [{ index: 0, as_text: true }];
```

## Conversions

`intosql.conversions` exposes each step on its own, for when you want to upload from somewhere else, start later, or not wait.

| Method                                               | Request                           | Returns                      |
| ---------------------------------------------------- | --------------------------------- | ---------------------------- |
| `create({ name, bytes, spec })`                      | `POST /conversions`               | `Conversion` with `upload`   |
| `upload(conversion, file)`                           | `PUT` to `conversion.upload.url`  | `void`                       |
| `start(id)`                                          | `POST /conversions/{id}/start`    | `Conversion`, `queued`       |
| `createFromUrl({ url, spec, name? })`                | `POST /conversions` with `url`    | `Conversion`, `queued`       |
| `retrieve(id)`                                       | `GET /conversions/{id}`           | `Conversion`                 |
| `wait(id, options?)`                                 | `GET /conversions/{id}`, repeated | `Conversion`, `done`         |
| `list({ limit?, status? }?)`                         | `GET /conversions`, every page    | `AsyncGenerator<Conversion>` |
| `cancel(id)`                                         | `POST /conversions/{id}/cancel`   | `Conversion`, `canceled`     |
| `delete(id)`                                         | `DELETE /conversions/{id}`        | `void`                       |
| `createCase(id, { consent, share?, email?, note? })` | `POST /conversions/{id}/cases`    | `Case`                       |

### Step by step

```ts
const file = await openAsBlob('ventas.xlsx');

const opened = await intosql.conversions.create({
    name: 'ventas.xlsx',
    bytes: file.size,
    spec: spec({ dialect: 'mariadb' }),
});

await intosql.conversions.upload(opened, file);
await intosql.conversions.start(opened.id);

// Later, or from a webhook handler:
const conversion = await intosql.conversions.retrieve(opened.id);
```

The upload address from `create` works for an hour. `upload` throws if the conversion has no upload address (one read with `retrieve` never does), and throws an `IntoSQLError` with code `upload_failed` if storage doesn't answer with a 2xx.

### List

`list()` yields every conversion on the account, newest first. It fetches a page at a time (50, or `limit` from 1 to 100) and follows `links.next` until there are no more pages. `status` keeps only conversions in that status.

```ts
for await (const conversion of intosql.conversions.list({ status: 'failed' })) {
    console.log(conversion.id, conversion.error?.code, conversion.name);
}
```

Stop early with `break`; no further pages are fetched.

### Cancel

`cancel(id)` stops a conversion that hasn't ended and deletes its files. It stays listed as `canceled` and doesn't count toward the month. Canceling it again answers the same; canceling one that already ended throws `IntoSQLError` with code `already_finished`.

```ts
await intosql.conversions.cancel(conversion.id);
```

### Send us a conversion

When a file didn't convert well, or at all, `createCase` sends the conversion to IntoSQL so support for it gets added. Pass `consent: true` only when the person agreed to send it. `share: 'file'` sends the file as well as the report; a failed conversion keeps its upload until it expires, so the file is still there to send.

```ts
await intosql.conversions.createCase(conversion.id, {
    consent: true,
    share: 'file',
    note: 'Exported from our ERP; the dates come out wrong.',
});
```

Cases are private, kept 90 days, and then deleted. The [privacy policy](/privacy) says how they are handled.

### Delete

`delete(id)` removes the conversion's input and output files now instead of after 24 hours. Reading it afterwards answers `not_found`.

```ts
await intosql.conversions.delete(conversion.id);
```

## Usage

`usage()` returns the bytes the plan includes this month, used and left.

```ts
const { limit, used, left, period_start } = await intosql.usage();
```

Failed conversions don't count toward `used`.

## Verify webhooks

With a webhook address saved in Settings, API keys, IntoSQL sends a signed `POST` when a conversion is done, failed or canceled. `parseWebhook` checks the `IntoSQL-Signature` header against the raw body and returns the typed event; it throws when the signature doesn't match.

```ts
import { parseWebhook } from '@intosql/sdk';

export async function POST(request: Request): Promise<Response> {
    let event;

    try {
        event = await parseWebhook(
            await request.text(),
            request.headers.get('IntoSQL-Signature') ?? '',
            process.env.INTOSQL_WEBHOOK_SECRET!,
        );
    } catch {
        return new Response(null, { status: 400 });
    }

    if (event.type === 'conversion.done') {
        // event.data is the conversion; event.id is the same on every retry.
    }

    return new Response(null, { status: 204 });
}
```

`verifyWebhook(body, header, secret)` does the check alone and resolves to `true` or `false`. Both refuse a call when the header is malformed, the signature doesn't match, or the timestamp is more than 300 seconds from now. The fourth argument takes `toleranceSeconds` to change that window and `now` (Unix seconds) to fix the current time in tests. Pass the body exactly as received, before any JSON parsing. The payload and retries are in [Webhooks](/docs/api/webhooks).

## Errors

The SDK throws two error classes.

- **`IntoSQLError`**: the API answered with an error. It has `status` (the HTTP status), `code` (stable between versions), `message` (for people), `fields` (per field messages when the code is `invalid_request`, otherwise `{}`) and `requestId`, to quote when you write to us. If the answer has no error body, `code` is `http_error`.
- **`ConversionFailedError`**: the conversion ended as `failed`. It has `id`, `code` (stable: ours, such as `timed_out`, or the engine's, such as `encrypted_file`), `problems` (what the engine found) and the explanation as its `message`.
- **`ConversionCanceledError`**: the conversion was canceled while it was awaited. It has `id`.

A connection that keeps failing after the retries throws the runtime's own error (a `TypeError` from `fetch`). Waiting past `timeoutMs` throws a plain `Error`.

```ts
import { ConversionFailedError, IntoSQLError } from '@intosql/sdk';

try {
    await intosql.convert(await openAsBlob('ventas.xlsx'), {
        name: 'ventas.xlsx',
        spec: spec({ dialect: 'oracle' }),
    });
} catch (error) {
    if (error instanceof ConversionFailedError) {
        console.error(
            `Conversion ${error.id} failed (${error.code}): ${error.message}`,
        );
    } else if (error instanceof IntoSQLError && error.code === 'over_quota') {
        console.error("This month's conversions are used up.");
    } else if (error instanceof IntoSQLError) {
        console.error(error.status, error.code, error.message, error.fields);
    } else {
        throw error;
    }
}
```

Every code, its status and how to fix it is in [Errors](/docs/api/errors).

## Retries and idempotency

The client retries a request when the connection fails, when the API answers `rate_limited`, `too_many_active` or `request_in_progress`, on any 5xx, and on 408 and 425 from the network in between, up to `maxRetries` times (2 by default). It waits for `Retry-After` when the answer has it, and otherwise half a second, then 1 second, doubling up to 8 seconds.

Every `POST` gets an `Idempotency-Key` header with a random UUID, and its retries reuse the same key. If a first attempt reached the API but its answer was lost, the retry gets that first answer back instead of opening a second conversion.

Not retried: the file upload to storage (a failed `PUT` throws `upload_failed`), `already_started` (the conversion is on its way; read it instead), and any other 4xx such as `invalid_spec` or `over_quota`.

## Types

The package exports these types alongside the classes and functions:

| Type                      | What it is                                                                                                                                                                   |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `IntoSQLOptions`          | The constructor's options.                                                                                                                                                   |
| `WaitOptions`             | `{ timeoutMs?, signal? }`.                                                                                                                                                   |
| `SpecOptions`             | The options of `spec()`.                                                                                                                                                     |
| `Conversion`              | A conversion as the API answers it: `id`, `status`, `name`, `bytes`, `spec`, `report`, `error`, `output`, `upload`, `created_at`, `started_at`, `finished_at`, `expires_at`. |
| `ConversionFailure`       | A failed conversion's `error`: `{ code, message, problems }`.                                                                                                                |
| `Case`                    | A conversion sent to IntoSQL: `id`, `conversion_id`, `share`, `status`, `created_at`, `expires_at`.                                                                          |
| `WebhookEvent`            | What `parseWebhook` returns: `{ id, type, created_at, data }`.                                                                                                               |
| `ConversionStatus`        | `'awaiting_upload' \| 'queued' \| 'running' \| 'done' \| 'failed' \| 'canceled'`.                                                                                            |
| `Upload`                  | `Blob \| Uint8Array \| { stream: ReadableStream<Uint8Array>; bytes: number }`.                                                                                               |
| `Usage`                   | `{ limit, used, left, period_start }`.                                                                                                                                       |
| `Page<T>`                 | `{ data: T[]; links: { next: string \| null }; meta: { per_page, next_cursor, prev_cursor } }`.                                                                              |
| `JobSpec` and its parts   | `InputSpec`, `OutputSpec`, `InferenceSpec`, `ColumnSpec`, `Dialect`, `OutputFormat`, `OutputStyle`, `ColumnNames`, `DateOrder`, `InputFormat`.                               |
| `JobReport` and its parts | `ColumnReport`, `ColumnKind`, `RowCounts`, `Fix`, `FixCode`, `Problem`, `ErrorCode`, `Severity`, `DetectedInput`, `Timings`.                                                 |

The `JobSpec` and `JobReport` types are generated from the engine's contract, so they match what the API accepts and returns. `report` is described in [Conversions](/docs/api/conversions), and fixes in [Fixes](/docs/fixes).
