---
title: "Ingest and serve API"
description: "Send rows into a dataset, start a pipeline run with parameters and read its results, and read the latest rows of a dataset, all over plain HTTPS with a secret URL."
url: "https://www.jsonify.com/docs/integrations/api"
---

[Documentation](https://www.jsonify.com/docs/) Filter pages

## Getting started

*   [What is Jsonify?](https://www.jsonify.com/docs/)
*   [How pipelines work](https://www.jsonify.com/docs/how-jsonify-works)
*   [Your first pipeline](https://www.jsonify.com/docs/getting-started/first-pipeline)
*   [Workspaces](https://www.jsonify.com/docs/getting-started/workspaces)
*   [Glossary](https://www.jsonify.com/docs/getting-started/glossary)

## Tutorials

*   [Track the Hacker News front page](https://www.jsonify.com/docs/tutorials/hacker-news)
*   [Monitor prices across retailers](https://www.jsonify.com/docs/tutorials/price-monitoring)
*   [Get a Slack alert when a price drops](https://www.jsonify.com/docs/tutorials/slack-price-alert)
*   [Use a dataset from your agent](https://www.jsonify.com/docs/tutorials/agent-query)

## Jason

*   [Chatting with Jason](https://www.jsonify.com/docs/jason/chat)
*   [Changing a pipeline](https://www.jsonify.com/docs/jason/changing-a-pipeline)
*   [Agents, workers and repairs](https://www.jsonify.com/docs/data-agents)

## Pipelines

*   [The pipeline page](https://www.jsonify.com/docs/pipelines/overview)
*   [Statement of work and steps](https://www.jsonify.com/docs/pipelines/statement-of-work)
*   [Inputs and parameters](https://www.jsonify.com/docs/pipelines/inputs)
*   [Settings reference](https://www.jsonify.com/docs/pipelines/settings)
*   [Versions and changelog](https://www.jsonify.com/docs/pipelines/versions)

## Runs

*   [Running a pipeline](https://www.jsonify.com/docs/runs/running)
*   [Scheduling](https://www.jsonify.com/docs/runs/scheduling)
*   [Run detail](https://www.jsonify.com/docs/runs/run-detail)
*   [Run lifecycle](https://www.jsonify.com/docs/runs/lifecycle)
*   [Verification and repair](https://www.jsonify.com/docs/runs/verification-and-repair)

## Datasets

*   [Dataset pages](https://www.jsonify.com/docs/datasets/overview)
*   [Versions](https://www.jsonify.com/docs/datasets/versions)
*   [Query and export](https://www.jsonify.com/docs/datasets/query-and-export)
*   [Row detail and provenance](https://www.jsonify.com/docs/datasets/provenance)
*   [Sharing](https://www.jsonify.com/docs/datasets/sharing)

## Analytics

*   [Dashboards](https://www.jsonify.com/docs/analytics/dashboards)
*   [Newsletters](https://www.jsonify.com/docs/analytics/newsletters)

## Alerts

*   [Alert rules](https://www.jsonify.com/docs/alerts/rules)
*   [Alert history and notifications](https://www.jsonify.com/docs/alerts/history)

## Integrations

*   [Integrations overview](https://www.jsonify.com/docs/integrations/overview)
*   [Slack and email](https://www.jsonify.com/docs/integrations/slack-and-email)
*   [Sheets, warehouses and webhooks](https://www.jsonify.com/docs/integrations/destinations)
*   [Ingest and serve API](https://www.jsonify.com/docs/integrations/api)
*   [Connect your agent (MCP)](https://www.jsonify.com/docs/integrations/mcp)
*   [Command line](https://www.jsonify.com/docs/integrations/cli)

## Workspace and account

*   [Workspace settings](https://www.jsonify.com/docs/workspace/settings)
*   [Team](https://www.jsonify.com/docs/workspace/team)
*   [Billing and usage](https://www.jsonify.com/docs/workspace/billing)
*   [Data retention](https://www.jsonify.com/docs/workspace/data-retention)

## Plans

*   [Radar or Benchmark? Enterprise](https://www.jsonify.com/docs/radar-vs-benchmark)
*   [Row pricing and managed service](https://www.jsonify.com/docs/pricing-model)

## More

*   [Connect your agent](https://www.jsonify.com/connect)
*   [Pricing](https://www.jsonify.com/pricing)
*   [Contact support](https://www.jsonify.com/contact)

[Docs](https://www.jsonify.com/docs/)/Integrations

# Ingest and serve API

Send rows into a dataset, start a pipeline run with parameters and read its results, and read the latest rows of a dataset, all over plain HTTPS with a secret URL.

Three integrations give a workspace an HTTP surface without any sign-in dance. Each one is a single secret URL created in Jsonify.

| Integration | Direction | URL shape | Purpose |
| --- | --- | --- | --- |
| Receive JSON webhook | You send rows in | POST https://factory.jsonify.com/api/ingest/<id> | Each request writes a new version of a dataset worksheet |
| Pipeline Trigger | You start a run | POST https://factory.jsonify.com/api/ingest/<id> | Starts one pipeline, optionally with parameters, and gives you URLs to follow it |
| Publish Dataset API | You read rows out | GET https://factory.jsonify.com/api/serve/<id> | Returns the latest version of a dataset worksheet as JSON |

`<id>` is the integration’s own identifier, shown on its detail page. A fourth integration, **Fetch external JSON**, goes the other way round: Jsonify fetches rows from a URL of yours. It is covered [at the end](https://www.jsonify.com/docs/integrations/api#fetch-rows-from-your-own-endpoint).

> The URL is the secret. There is no separate key, header or signature: anyone who has the URL can use it until the integration is deleted. Keep it out of shared documents and front-end pages, and if it leaks, delete the integration and create a new one; the old URL stops resolving the moment you delete it.

1.  **Create**pick the dataset or pipeline
2.  **Copy the URL**Overview tab
3.  **Call it**POST or GET
4.  **Follow up**new version, or run status and results

Every endpoint on this page is created the same way.

## Receive rows over a webhook

**Receive JSON webhook** gives a dataset worksheet an address that other systems can post rows to: a form tool, an internal service, a partner’s export, a scheduled job.

### Set it up

| Option | Values | Default | What it does |
| --- | --- | --- | --- |
| Label | Text | “Receive JSON webhook” | How the integration appears in pickers |
| Target dataset | A dataset, or + Create new dataset | Required | Rows posted to the URL land here |
| Target worksheet | A worksheet of that dataset | Required | Which worksheet receives them |

Press **Connect**; there is nothing to test. The integration is **Live** at once and its **Overview** tab shows the **Ingest endpoint** block with the URL, the dataset’s current version and when it last changed. The detail page’s **Code** tab holds a cURL sample and a second sample for your own program, each with a **Copy** button.

![A Receive JSON webhook integration's Overview tab showing the Ingest endpoint block with the URL and a Copy link](/assets/images/product/integrations-ingest-detail.png)

The ingest endpoint. Copy the URL from here; it is the only credential.

### Send rows

Post one JSON object, or an array of objects. Each object is a row; its keys are column names.

```
curl -X POST https://factory.jsonify.com/api/ingest/int7f2c9a1b3 \
  -H 'Content-Type: application/json' \
  -d '[
    {"product_url": "https://www.example-store.com/p/12345", "retailer": "Example Store", "price": 149.0, "currency": "GBP"},
    {"product_url": "https://www.example-store.com/p/12399", "retailer": "Example Store", "price": 189.0, "currency": "GBP"}
  ]'
```

The answer confirms what was written:

```
{"ok": true, "integration": "int7f2c9a1b3", "dataset": "dsa41b7c9e2f0", "version": 18, "rows_ingested": 2}
```

Every successful request creates a new [dataset version](https://www.jsonify.com/docs/datasets/versions). If the dataset has key columns, incoming rows replace rows with the same key and new keys are added; if it has none, the rows are appended below the existing ones. Keys become column names, so keep them consistent between requests. Anything in the array that is not an object is dropped.

After the version is written, the dataset does everything it does after a pipeline publish: alert rules are evaluated, sinks deliver, dashboards recompute. The dataset page shows the version with the source **Ingested**.

### Errors

| Answer | Meaning |
| --- | --- |
| 400 “response must be a JSON object or array of objects” | The body was not JSON, or not an object or array |
| 400 “No rows to write” | An empty array, or an array with no objects in it |
| 400 “Integration is not live” | The integration is not Live. Open it and check its status |
| 400 “This integration is configured for pull mode” | You posted to a Fetch external JSON integration, which is read by Jsonify, not written to |
| 404 “Integration not found” | Wrong URL, or the integration was deleted |
| 404 “Target dataset not found” | The dataset was deleted. Open the integration and pick another |

> A partner emails a nightly CSV, and a small job on your side converts it to JSON and posts it to the ingest URL. The dataset **Partner stock** gains a version each night, an alert rule watches for products that disappeared, and a sheet mirrors it for the buying team. No pipeline involved.

## Start a run from a webhook

**Pipeline Trigger** gives one pipeline an address that starts a run. Use it from a scheduler you already have, a deploy step, a Zapier zap, or anything that can send a request.

### Set it up

| Option | Values | Default | What it does |
| --- | --- | --- | --- |
| Label | Text | “Run pipeline on POST” | How the integration appears in pickers |
| Pipeline to run | A pipeline with an active version | Required | Which pipeline each request starts |

Press **Connect**. The **Overview** tab shows the **Trigger endpoint** block with the URL, followed by the pipeline’s parameters: each one’s name, type, default and description. Those are the fields you may send in the body. The pipeline’s own **Run** dialog also shows a banner, “Pipeline Trigger attached”, linking back to the integration, so anyone running it by hand knows it can also be started from outside.

### Start a run

An empty body starts the pipeline with its defaults, exactly as pressing **Run** would:

```
curl -X POST https://factory.jsonify.com/api/ingest/intd3e8f1a2b4
```

To pass parameters, send them as top-level JSON fields. Two optional run controls sit alongside them: `row_limit` (the row cap for this run; `0` means a full run) and `pipeline_version` (run a specific version rather than the active one).

```
curl -X POST https://factory.jsonify.com/api/ingest/intd3e8f1a2b4 \
  -H 'Content-Type: application/json' \
  -d '{"region": "uk", "include_out_of_stock": false, "row_limit": 0}'
```

The nested form `{"params": {"region": "uk"}, "row_limit": 0}` is also accepted. Do not send the same parameter both ways; the request is refused. Parameters are validated exactly as in the [Run dialog](https://www.jsonify.com/docs/runs/running); a wrong type or an unknown name is a `400` with the reason.

The answer arrives as soon as the run is queued:

```
{
  "ok": true,
  "run_id": "mrun00000361",
  "pipeline": "marketpipe03",
  "status": "queued",
  "status_url": "https://factory.jsonify.com/api/ingest/intd3e8f1a2b4/runs/mrun00000361"
}
```

Only one run per pipeline can be active. While one is queued or running, another request gets `409` with `"error": "Pipeline already has an active run"` plus the active `run_id` and its status; nothing is queued behind it. Wait for the status URL to report a finished run, then post again.

Runs started this way show the trigger **webhook** on the [runs list](https://www.jsonify.com/docs/runs/run-detail). If one fails persistently, it is eligible for [automatic repair](https://www.jsonify.com/docs/runs/verification-and-repair) in the same way as a scheduled run.

### Check the status

`status_url` is specific to the run and to the trigger that started it. Fetch it until `status` is `completed`, `failed` or `cancelled`. Every couple of seconds is plenty.

```
curl https://factory.jsonify.com/api/ingest/intd3e8f1a2b4/runs/mrun00000361
```

```
{
  "ok": true,
  "run_id": "mrun00000361",
  "pipeline": "marketpipe03",
  "status": "completed",
  "created_at": 1789452131,
  "finished_at": 1789452498,
  "results": [
    {
      "dataset": "marketdata02",
      "dataset_name": "Price observations",
      "version": 61,
      "table": "main",
      "row_count": 1184,
      "result_url": "https://factory.jsonify.com/api/ingest/intd3e8f1a2b4/runs/mrun00000361/results/marketdata02?table=main"
    }
  ],
  "results_truncated": false
}
```

`results` is empty until the run completes. A failed run adds an `error` field. A run that published to more than 100 datasets lists the first 100 and sets `results_truncated` to `true`; open the run in Jsonify for the rest.

### Read the results

Each `result_url` returns the exact dataset version the run produced, a page at a time. `limit` is at most 100 rows; follow `next_url` while `has_more` is `true`.

```
curl 'https://factory.jsonify.com/api/ingest/intd3e8f1a2b4/runs/mrun00000361/results/marketdata02?table=main&offset=0&limit=100'
```

```
{
  "ok": true,
  "run_id": "mrun00000361",
  "pipeline": "marketpipe03",
  "dataset": "marketdata02",
  "dataset_name": "Price observations",
  "version": 61,
  "table": "main",
  "row_count": 1184,
  "offset": 0,
  "limit": 100,
  "rows": [ { "...": "..." } ],
  "has_more": true,
  "next_url": "https://factory.jsonify.com/api/ingest/intd3e8f1a2b4/runs/mrun00000361/results/marketdata02?table=main&offset=100&limit=100"
}
```

Asking for results before the run has completed answers `409` “Trigger run is not completed”. Because the version is pinned, you can page through it at leisure; a later run does not change what this URL returns.

| Answer | Meaning |
| --- | --- |
| 400 “Invalid run config:” | A parameter failed validation, or was supplied both directly and inside params |
| 409 “Pipeline already has an active run” | Wait for the current run, then post again |
| 409 “Pipeline has no active revision” | The pipeline has never been activated; finish its first build in Jsonify |
| 404 “Trigger run not found” | The run was not started by this trigger, or the identifier is wrong |
| 409 “Trigger run is not completed” | Results were requested before the run finished |

> A deployment step posts to the trigger after each catalogue import, waits on the status URL, then pages the result into the search index. The run’s row cap, parameters and version are all in the request, so the step needs nothing from the Jsonify UI.

## Serve a dataset over HTTP

**Publish Dataset API** gives a dataset worksheet an address that returns its latest rows. Your app, your BI tool’s web connector, or a scheduled loader fetches it whenever it likes.

### Set it up

| Option | Values | Default | What it does |
| --- | --- | --- | --- |
| Label | Text | “Publish dataset API” | How the integration appears in pickers |
| Source dataset | A dataset | Required | The dataset whose rows are returned |
| Source worksheet | A worksheet of that dataset | Required | Which worksheet is returned |

Press **Connect**. The **Overview** tab shows the **Serve endpoint** block with the URL and the dataset’s current version.

### Read rows

```
curl https://factory.jsonify.com/api/serve/int9c4e2b7d1a
```

```
{
  "integration": "int9c4e2b7d1a",
  "dataset": "marketdata01",
  "dataset_name": "Live product offers",
  "table": "main",
  "version": 42,
  "updated_at": 1789452498,
  "row_count": 3120,
  "rows": [
    {"product_url": "https://www.example-store.com/p/12345", "retailer": "Example Store", "title": "Espresso machine 15 bar", "price": 149.0, "currency": "GBP", "in_stock": true},
    {"...": "..."}
  ]
}
```

The endpoint always returns the **active** version of the worksheet, in full, as JSON. There are no query parameters: no paging, no filters, no other formats. A dataset that has not published yet answers with `"version": null` and an empty `rows` array. Compare `version` between fetches to know whether anything changed.

For a filtered or paged read, or CSV, use the [command line](https://www.jsonify.com/docs/integrations/cli) or an [MCP assistant](https://www.jsonify.com/docs/integrations/mcp) instead; both sign in with a credential rather than a URL.

> Serve URLs are read by whoever holds them. Do not embed one in a public web page or a shared notebook. For sharing a dataset with people, use a [share link](https://www.jsonify.com/docs/datasets/sharing), which can be turned off and shows a proper page.

## Fetch rows from your own endpoint

**Fetch external JSON** is the mirror of the ingest webhook: instead of you posting rows, Jsonify fetches them from a URL you own and writes them into a dataset. Use it for a read-only endpoint that already exists.

| Option | Values | Default | What it does |
| --- | --- | --- | --- |
| Label | Text | “Fetch external JSON” | How the integration appears in pickers |
| Endpoint URL | An http:// or https:// URL | Required | Where rows are fetched from. Must return JSON |
| Target dataset | A dataset, or + Create new dataset | Required | Where the rows go |
| Target worksheet | A worksheet of that dataset | Required | Which worksheet receives them |
| Auth header (optional) | A full header line, for example Authorization: Bearer … | Empty | Sent on every fetch. Leave blank for an open endpoint |

**Connect** fetches the endpoint once, without the auth header, and checks the answer is JSON. If your endpoint refuses unauthenticated requests, the check fails; make it answer with an empty JSON array for unauthenticated calls, or allow Jsonify’s check through some other way, then save.

The endpoint may return an array of objects, or an object whose `data`, `results`, `items` or `rows` key holds the array.

Like a pulled sheet, this source has no timer of its own: Jsonify fetches it before every run of a pipeline that uses the dataset as an input, and when you sync the dataset by hand. Each fetch that returns rows writes a new version, merged by key when the dataset has key columns. See [Pull rows from a sheet](https://www.jsonify.com/docs/integrations/destinations#pull-rows-from-a-sheet) for the same behaviour explained for sheets.

## Keeping URLs private

*   **Store the URL where you keep secrets.** A CI secret, a secret manager, an environment variable. Not in the request log of a shared tool.
*   **One integration per caller.** If two systems post to the same dataset, give each its own Receive JSON webhook. Then a leak, or a decommissioned system, costs you one delete rather than a migration.
*   **Rotate by replacing.** There is no rotate button for a URL. Create a new integration, move the caller to the new URL, then delete the old one. The old URL stops working the moment it is deleted; nothing else changes.
*   **Watch for use.** Ingest and trigger activity shows up as dataset versions with the source **Ingested** and as runs with the trigger **webhook**. An unexpected one is the signal to replace the URL.

## What’s next

[**Running a pipeline**

Row caps, versions and parameters, as the Run dialog offers them.

](https://www.jsonify.com/docs/runs/running)[**Dataset versions**

Why every ingest and every run is a new version, and how to look back.

](https://www.jsonify.com/docs/datasets/versions)[**Connect your agent (MCP)**

Read datasets and start runs from ChatGPT, Claude or another assistant.

](https://www.jsonify.com/docs/integrations/mcp)[**Command line**

Filtered reads, exports and runs from a terminal or a job.

](https://www.jsonify.com/docs/integrations/cli)

[PreviousSheets, warehouses and webhooks](https://www.jsonify.com/docs/integrations/destinations) [NextConnect your agent (MCP)](https://www.jsonify.com/docs/integrations/mcp)

Something missing or wrong? [Tell us](https://www.jsonify.com/contact).[Build it →](https://factory.jsonify.com/setup)

## On this page
