SDKs
TypeScript SDK
Convert spreadsheets, CSV and JSON into SQL from TypeScript or JavaScript with @intosql/sdk - upload, wait, download, webhooks and errors.
@intosql/sdk wraps the IntoSQL API for TypeScript and JavaScript, one of the official 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 and keep it out of your source code.
Install
npm install @intosql/sdkThe package is ESM only and runs on Node 20 or newer, Deno, Bun and edge runtimes.
Create a client
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.
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:
- Open.
POST /conversionswithname, the file's size in bytes andspec. The answer has an upload address. - Upload. The file is sent with a
PUTto that address, with the headers the API gave, as it is. Nothing is read into memory first. - Start.
POST /conversions/{id}/start. - Wait. The conversion is read until its status is
doneorfailed.
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.
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.
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.
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.
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.
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.
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.
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
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.
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.
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.
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 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.
await intosql.conversions.delete(conversion.id);Usage
usage() returns the bytes the plan includes this month, used and left.
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.
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.
Errors
The SDK throws two error classes.
IntoSQLError: the API answered with an error. It hasstatus(the HTTP status),code(stable between versions),message(for people),fields(per field messages when the code isinvalid_request, otherwise{}) andrequestId, to quote when you write to us. If the answer has no error body,codeishttp_error.ConversionFailedError: the conversion ended asfailed. It hasid,code(stable: ours, such astimed_out, or the engine's, such asencrypted_file),problems(what the engine found) and the explanation as itsmessage.ConversionCanceledError: the conversion was canceled while it was awaited. It hasid.
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.
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.
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, and fixes in Fixes.