curl --request POST \
--url https://origami.chat/api/v2/tables/{tableId}/rows/upsert \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"rows": [
{}
],
"matchColumns": [
"<string>"
],
"enrich": true,
"reenrichUpdated": false,
"batchId": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
}
'import requests
url = "https://origami.chat/api/v2/tables/{tableId}/rows/upsert"
payload = {
"rows": [{}],
"matchColumns": ["<string>"],
"enrich": True,
"reenrichUpdated": False,
"batchId": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
rows: [{}],
matchColumns: ['<string>'],
enrich: true,
reenrichUpdated: false,
batchId: '3c90c3cc-0d44-4b50-8888-8dd25736052a'
})
};
fetch('https://origami.chat/api/v2/tables/{tableId}/rows/upsert', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://origami.chat/api/v2/tables/{tableId}/rows/upsert",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'rows' => [
[
]
],
'matchColumns' => [
'<string>'
],
'enrich' => true,
'reenrichUpdated' => false,
'batchId' => '3c90c3cc-0d44-4b50-8888-8dd25736052a'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://origami.chat/api/v2/tables/{tableId}/rows/upsert"
payload := strings.NewReader("{\n \"rows\": [\n {}\n ],\n \"matchColumns\": [\n \"<string>\"\n ],\n \"enrich\": true,\n \"reenrichUpdated\": false,\n \"batchId\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://origami.chat/api/v2/tables/{tableId}/rows/upsert")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"rows\": [\n {}\n ],\n \"matchColumns\": [\n \"<string>\"\n ],\n \"enrich\": true,\n \"reenrichUpdated\": false,\n \"batchId\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://origami.chat/api/v2/tables/{tableId}/rows/upsert")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"rows\": [\n {}\n ],\n \"matchColumns\": [\n \"<string>\"\n ],\n \"enrich\": true,\n \"reenrichUpdated\": false,\n \"batchId\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n}"
response = http.request(request)
puts response.read_body{
"object": "enrichment_run",
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"batchId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"tableRunId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"counts": {
"inserted": 123,
"updated": 123,
"skipped": 123
}
}{
"object": "enrichment_run",
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"batchId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"tableRunId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"counts": {
"inserted": 123,
"updated": 123,
"skipped": 123
}
}Upsert rows on matchColumns (the single v2 batch write primitive)
v2’s one row-write endpoint; there is no bare per-row insert (an
insert-only call is just an upsert whose rows match nothing). All writes
go through api_batches and are idempotent per batchId.
Rows matching an existing row on every matchColumns value update
that row’s input cells; non-matching rows insert. Per-row
inserted/updated/skipped outcomes are recorded on the batch.
- Row keys and
matchColumnsare input column slugs (fromGET /tables/{tableId}/columns→columns[].slug), never display names. A non-slug row key →UNKNOWN_FIELDS. matchColumns(required) must be real input columns (UNKNOWN_COLUMN) and every match value must be present + non-empty on every row (MISSING_MATCH_VALUE).- Duplicate request keys →
DUPLICATE_MATCH_KEY; multiple existing rows match one key →AMBIGUOUS_MATCH(409). enrich(default true) enriches freshly inserted rows.reenrichUpdated(default false): set true to also re-run enrichment on rows the upsert updated (re-spends credits); off by default so an upsert never silently re-enriches matched rows.
Only input-kind columns are writable. Enrichment, score, and
sequence columns are populated automatically; sequences are
agent/column-generated only (see POST /tables/{tableId}/campaigns),
never set via this endpoint; passing one returns NON_INPUT_COLUMNS.
The response references the tracking object by id: it is an
enrichment_run the caller polls via GET /enrichment-runs/{runId}.
curl --request POST \
--url https://origami.chat/api/v2/tables/{tableId}/rows/upsert \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"rows": [
{}
],
"matchColumns": [
"<string>"
],
"enrich": true,
"reenrichUpdated": false,
"batchId": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
}
'import requests
url = "https://origami.chat/api/v2/tables/{tableId}/rows/upsert"
payload = {
"rows": [{}],
"matchColumns": ["<string>"],
"enrich": True,
"reenrichUpdated": False,
"batchId": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
rows: [{}],
matchColumns: ['<string>'],
enrich: true,
reenrichUpdated: false,
batchId: '3c90c3cc-0d44-4b50-8888-8dd25736052a'
})
};
fetch('https://origami.chat/api/v2/tables/{tableId}/rows/upsert', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://origami.chat/api/v2/tables/{tableId}/rows/upsert",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'rows' => [
[
]
],
'matchColumns' => [
'<string>'
],
'enrich' => true,
'reenrichUpdated' => false,
'batchId' => '3c90c3cc-0d44-4b50-8888-8dd25736052a'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://origami.chat/api/v2/tables/{tableId}/rows/upsert"
payload := strings.NewReader("{\n \"rows\": [\n {}\n ],\n \"matchColumns\": [\n \"<string>\"\n ],\n \"enrich\": true,\n \"reenrichUpdated\": false,\n \"batchId\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://origami.chat/api/v2/tables/{tableId}/rows/upsert")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"rows\": [\n {}\n ],\n \"matchColumns\": [\n \"<string>\"\n ],\n \"enrich\": true,\n \"reenrichUpdated\": false,\n \"batchId\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://origami.chat/api/v2/tables/{tableId}/rows/upsert")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"rows\": [\n {}\n ],\n \"matchColumns\": [\n \"<string>\"\n ],\n \"enrich\": true,\n \"reenrichUpdated\": false,\n \"batchId\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n}"
response = http.request(request)
puts response.read_body{
"object": "enrichment_run",
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"batchId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"tableRunId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"counts": {
"inserted": 123,
"updated": 123,
"skipped": 123
}
}{
"object": "enrichment_run",
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"batchId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"tableRunId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"counts": {
"inserted": 123,
"updated": 123,
"skipped": 123
}
}Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your auth token.
Headers
Transport-level replay protection: any v2 POST may send it
(1-255 printable ASCII characters; UUIDs recommended). The first
response for a key is recorded and replayed verbatim for retries
with the same key + method/path/body for 24 hours; replayed
responses carry the Idempotency-Replay: true response header.
Reusing a key with a different request, or retrying while the
original is still in flight, returns 409 IDEMPOTENCY_ERROR.
5xx responses are never recorded (the retry re-executes).
1 - 255^[\x21-\x7e]{1,255}$Path Parameters
Body
Response
Idempotent replay of an existing batchId (same receipt shape).
Response of POST /tables/{tableId}/rows/upsert and
.../rows/upsert-file: an enrichment_run reference the caller
polls via GET /enrichment-runs/{runId}.
"enrichment_run"Legacy alias of id.
Linked table-run parent (enrich=true batches only). Lets consumers
poll GET /tables/{tableId}/runs/{runId} or await the
table.run.completed webhook without a version bump.
null for enrich=false writes and pre-migration batches.
Completion is first-owner scoped: overlapping cells already active
under an earlier run are not adopted into this parent, so this run
can complete while those cells are still executing elsewhere.
Show child attributes
Show child attributes