API reference
Generate & push
Generate rows, push them into a database, and poll long-running jobs.
Three ways to run a generation, differing only in what happens to the output.
| Endpoint | Output | Use when |
|---|---|---|
POST /api/generate |
returned to you | you want the data, not a write |
POST /api/generate/push |
written to the target, synchronously | small to medium volumes |
POST /api/generate/push-async |
queued as a job | large volumes, or CI that should not hold a connection |
Request
{
"connection": {
"db_type": "postgresql",
"connection_string": "postgresql://user:pass@host:5432/mydb",
"schema_name": "public"
},
"tables": [
{ "table_name": "users", "row_count": 500, "enabled": true, "column_overrides": {} }
],
"seed": 42
}
connection
db_type is postgresql, mysql or sqlite (default postgresql). schema_name defaults to public.
tables
table_name, must exist in the introspected schemarow_count, 1 to 500,000 per table, subject to your tierenabled, setfalseto skip without removing the entrycolumn_overrides, per-column generation hints, keyed by column name
seed
Optional. The same seed with the same config reproduces the same rows exactly. Omit it and one is chosen for you and returned as seed, so any run can be replayed later. Log it in CI.
Push-only fields
truncate_first empties the target tables before writing; truncate_cascade extends that through dependent tables. Both default to false.
Responses
POST /api/generate:
{
"total_rows": 5500,
"per_table": { "users": 500, "orders": 5000 },
"elapsed_seconds": 0.42,
"seed": 42
}
POST /api/generate/push:
{
"tables_processed": 2,
"total_rows": 5500,
"requested_rows": 5500,
"per_table": { "users": 500, "orders": 5000 },
"pushed_to_db": true,
"errors": [],
"cascaded_tables": [],
"cascaded_rows": 0
}
Read pushed_to_db, not the status code. It is derived from tables actually written. A run where every row hit a constraint returns 200 with pushed_to_db: false and the reasons in errors. An earlier version reported success there, which is the sort of thing that gets discovered at 6pm.
Compare total_rows with requested_rows to catch partial writes.
Async pushes
curl -s "$SEEDQL_API/api/generate/push-async" -X POST \
-H "Authorization: Bearer $SEEDQL_KEY" \
-H "Content-Type: application/json" \
-d '{ "connection": {...}, "tables": [...] }'
202 Accepted:
{
"job_id": "9d2f…",
"status": "queued",
"rows_requested": 500000,
"poll": "/api/jobs/9d2f…"
}
Poll it:
curl -s "$SEEDQL_API/api/jobs/$JOB_ID" -H "Authorization: Bearer $SEEDQL_KEY"
Status moves queued → running → completed, or failed / cancelled. Cancel with POST /api/jobs/{id}/cancel; the runner stops at the next safe checkpoint rather than mid-write.
Jobs are visible to their creator, and to org admins and owners.
Foreign keys
You never declare table order. SeedQL builds a dependency graph from the real foreign keys and runs a topological sort (Kahn's algorithm), generating parents before children so every child row references a parent that exists. Ties break alphabetically, which keeps a seeded run byte-identical across machines. Cycles are detected and broken explicitly rather than silently producing orphans.
Limits
| Tier | Rows per request | Rows per month |
|---|---|---|
| Hobby | 5,000 | 5,000 |
| Pro | 100,000 | 100,000 |
| Business / Platinum | 500,000 | 500,000 |
| Enterprise | 10,000,000 | effectively unlimited |
Exceeding a limit returns 402 with the tier and the cap in the body. Overage draws from your bonus-row balance first, if you have one.
Known gap: distributions
Generation is uniform. Foreign key values are chosen evenly across available parents, so no account ends up holding a disproportionate share of rows.
If a load test depends on that skew, one customer with 40,000 of 500,000 orders, SeedQL will not produce it today. Weighted distributions are on the roadmap and will be announced in the changelog, not quietly added. The practical workaround is to generate the bulk here and concentrate rows onto a chosen parent with an UPDATE, which is a few lines of SQL against a structurally valid database.