curl --request PUT \
--url https://api.qobra.co/v1/users/bulk \
--header 'Content-Type: application/json' \
--header 'X-API-Key: <api-key>' \
--data '
{
"data": [
{
"email": "user-test-1@company.com",
"name": "User 1",
"role": "Sales Rep",
"currency": "EUR",
"calculation_currency": "USD",
"country": "France",
"code": "AO32ND56",
"arrival_date": "2023-12-01",
"junior": true
},
{
"email": "user-test-2@company.com",
"name": "User 2",
"role": "Manager",
"currency": "USD",
"calculation_currency": "USD",
"country": "Germany",
"code": "AO32ND57",
"arrival_date": "2024-01-01",
"junior": false
},
"..."
]
}
'import requests
url = "https://api.qobra.co/v1/users/bulk"
payload = { "data": [
{
"email": "user-test-1@company.com",
"name": "User 1",
"role": "Sales Rep",
"currency": "EUR",
"calculation_currency": "USD",
"country": "France",
"code": "AO32ND56",
"arrival_date": "2023-12-01",
"junior": True
},
{
"email": "user-test-2@company.com",
"name": "User 2",
"role": "Manager",
"currency": "USD",
"calculation_currency": "USD",
"country": "Germany",
"code": "AO32ND57",
"arrival_date": "2024-01-01",
"junior": False
},
"..."
] }
headers = {
"X-API-Key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.put(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'PUT',
headers: {'X-API-Key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
data: [
{
email: 'user-test-1@company.com',
name: 'User 1',
role: 'Sales Rep',
currency: 'EUR',
calculation_currency: 'USD',
country: 'France',
code: 'AO32ND56',
arrival_date: '2023-12-01',
junior: true
},
{
email: 'user-test-2@company.com',
name: 'User 2',
role: 'Manager',
currency: 'USD',
calculation_currency: 'USD',
country: 'Germany',
code: 'AO32ND57',
arrival_date: '2024-01-01',
junior: false
},
'...'
]
})
};
fetch('https://api.qobra.co/v1/users/bulk', 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://api.qobra.co/v1/users/bulk",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_POSTFIELDS => json_encode([
'data' => [
[
'email' => 'user-test-1@company.com',
'name' => 'User 1',
'role' => 'Sales Rep',
'currency' => 'EUR',
'calculation_currency' => 'USD',
'country' => 'France',
'code' => 'AO32ND56',
'arrival_date' => '2023-12-01',
'junior' => true
],
[
'email' => 'user-test-2@company.com',
'name' => 'User 2',
'role' => 'Manager',
'currency' => 'USD',
'calculation_currency' => 'USD',
'country' => 'Germany',
'code' => 'AO32ND57',
'arrival_date' => '2024-01-01',
'junior' => false
],
'...'
]
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"X-API-Key: <api-key>"
],
]);
$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://api.qobra.co/v1/users/bulk"
payload := strings.NewReader("{\n \"data\": [\n {\n \"email\": \"user-test-1@company.com\",\n \"name\": \"User 1\",\n \"role\": \"Sales Rep\",\n \"currency\": \"EUR\",\n \"calculation_currency\": \"USD\",\n \"country\": \"France\",\n \"code\": \"AO32ND56\",\n \"arrival_date\": \"2023-12-01\",\n \"junior\": true\n },\n {\n \"email\": \"user-test-2@company.com\",\n \"name\": \"User 2\",\n \"role\": \"Manager\",\n \"currency\": \"USD\",\n \"calculation_currency\": \"USD\",\n \"country\": \"Germany\",\n \"code\": \"AO32ND57\",\n \"arrival_date\": \"2024-01-01\",\n \"junior\": false\n },\n \"...\"\n ]\n}")
req, _ := http.NewRequest("PUT", url, payload)
req.Header.Add("X-API-Key", "<api-key>")
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.put("https://api.qobra.co/v1/users/bulk")
.header("X-API-Key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"data\": [\n {\n \"email\": \"user-test-1@company.com\",\n \"name\": \"User 1\",\n \"role\": \"Sales Rep\",\n \"currency\": \"EUR\",\n \"calculation_currency\": \"USD\",\n \"country\": \"France\",\n \"code\": \"AO32ND56\",\n \"arrival_date\": \"2023-12-01\",\n \"junior\": true\n },\n {\n \"email\": \"user-test-2@company.com\",\n \"name\": \"User 2\",\n \"role\": \"Manager\",\n \"currency\": \"USD\",\n \"calculation_currency\": \"USD\",\n \"country\": \"Germany\",\n \"code\": \"AO32ND57\",\n \"arrival_date\": \"2024-01-01\",\n \"junior\": false\n },\n \"...\"\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.qobra.co/v1/users/bulk")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Put.new(url)
request["X-API-Key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"data\": [\n {\n \"email\": \"user-test-1@company.com\",\n \"name\": \"User 1\",\n \"role\": \"Sales Rep\",\n \"currency\": \"EUR\",\n \"calculation_currency\": \"USD\",\n \"country\": \"France\",\n \"code\": \"AO32ND56\",\n \"arrival_date\": \"2023-12-01\",\n \"junior\": true\n },\n {\n \"email\": \"user-test-2@company.com\",\n \"name\": \"User 2\",\n \"role\": \"Manager\",\n \"currency\": \"USD\",\n \"calculation_currency\": \"USD\",\n \"country\": \"Germany\",\n \"code\": \"AO32ND57\",\n \"arrival_date\": \"2024-01-01\",\n \"junior\": false\n },\n \"...\"\n ]\n}"
response = http.request(request)
puts response.read_body{
"import_id": "QfejsQZ6mpOVHT5Tt1SdNLPZ",
"objects_imported": 2
}{
"count": 1,
"errors": [
{
"error": "ValidationError",
"resource": "data",
"description": "Too many users, at most 200 can be sent per call"
}
]
}Upsert users bulk
Upsert users in bulk.
curl --request PUT \
--url https://api.qobra.co/v1/users/bulk \
--header 'Content-Type: application/json' \
--header 'X-API-Key: <api-key>' \
--data '
{
"data": [
{
"email": "user-test-1@company.com",
"name": "User 1",
"role": "Sales Rep",
"currency": "EUR",
"calculation_currency": "USD",
"country": "France",
"code": "AO32ND56",
"arrival_date": "2023-12-01",
"junior": true
},
{
"email": "user-test-2@company.com",
"name": "User 2",
"role": "Manager",
"currency": "USD",
"calculation_currency": "USD",
"country": "Germany",
"code": "AO32ND57",
"arrival_date": "2024-01-01",
"junior": false
},
"..."
]
}
'import requests
url = "https://api.qobra.co/v1/users/bulk"
payload = { "data": [
{
"email": "user-test-1@company.com",
"name": "User 1",
"role": "Sales Rep",
"currency": "EUR",
"calculation_currency": "USD",
"country": "France",
"code": "AO32ND56",
"arrival_date": "2023-12-01",
"junior": True
},
{
"email": "user-test-2@company.com",
"name": "User 2",
"role": "Manager",
"currency": "USD",
"calculation_currency": "USD",
"country": "Germany",
"code": "AO32ND57",
"arrival_date": "2024-01-01",
"junior": False
},
"..."
] }
headers = {
"X-API-Key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.put(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'PUT',
headers: {'X-API-Key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
data: [
{
email: 'user-test-1@company.com',
name: 'User 1',
role: 'Sales Rep',
currency: 'EUR',
calculation_currency: 'USD',
country: 'France',
code: 'AO32ND56',
arrival_date: '2023-12-01',
junior: true
},
{
email: 'user-test-2@company.com',
name: 'User 2',
role: 'Manager',
currency: 'USD',
calculation_currency: 'USD',
country: 'Germany',
code: 'AO32ND57',
arrival_date: '2024-01-01',
junior: false
},
'...'
]
})
};
fetch('https://api.qobra.co/v1/users/bulk', 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://api.qobra.co/v1/users/bulk",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_POSTFIELDS => json_encode([
'data' => [
[
'email' => 'user-test-1@company.com',
'name' => 'User 1',
'role' => 'Sales Rep',
'currency' => 'EUR',
'calculation_currency' => 'USD',
'country' => 'France',
'code' => 'AO32ND56',
'arrival_date' => '2023-12-01',
'junior' => true
],
[
'email' => 'user-test-2@company.com',
'name' => 'User 2',
'role' => 'Manager',
'currency' => 'USD',
'calculation_currency' => 'USD',
'country' => 'Germany',
'code' => 'AO32ND57',
'arrival_date' => '2024-01-01',
'junior' => false
],
'...'
]
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"X-API-Key: <api-key>"
],
]);
$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://api.qobra.co/v1/users/bulk"
payload := strings.NewReader("{\n \"data\": [\n {\n \"email\": \"user-test-1@company.com\",\n \"name\": \"User 1\",\n \"role\": \"Sales Rep\",\n \"currency\": \"EUR\",\n \"calculation_currency\": \"USD\",\n \"country\": \"France\",\n \"code\": \"AO32ND56\",\n \"arrival_date\": \"2023-12-01\",\n \"junior\": true\n },\n {\n \"email\": \"user-test-2@company.com\",\n \"name\": \"User 2\",\n \"role\": \"Manager\",\n \"currency\": \"USD\",\n \"calculation_currency\": \"USD\",\n \"country\": \"Germany\",\n \"code\": \"AO32ND57\",\n \"arrival_date\": \"2024-01-01\",\n \"junior\": false\n },\n \"...\"\n ]\n}")
req, _ := http.NewRequest("PUT", url, payload)
req.Header.Add("X-API-Key", "<api-key>")
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.put("https://api.qobra.co/v1/users/bulk")
.header("X-API-Key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"data\": [\n {\n \"email\": \"user-test-1@company.com\",\n \"name\": \"User 1\",\n \"role\": \"Sales Rep\",\n \"currency\": \"EUR\",\n \"calculation_currency\": \"USD\",\n \"country\": \"France\",\n \"code\": \"AO32ND56\",\n \"arrival_date\": \"2023-12-01\",\n \"junior\": true\n },\n {\n \"email\": \"user-test-2@company.com\",\n \"name\": \"User 2\",\n \"role\": \"Manager\",\n \"currency\": \"USD\",\n \"calculation_currency\": \"USD\",\n \"country\": \"Germany\",\n \"code\": \"AO32ND57\",\n \"arrival_date\": \"2024-01-01\",\n \"junior\": false\n },\n \"...\"\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.qobra.co/v1/users/bulk")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Put.new(url)
request["X-API-Key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"data\": [\n {\n \"email\": \"user-test-1@company.com\",\n \"name\": \"User 1\",\n \"role\": \"Sales Rep\",\n \"currency\": \"EUR\",\n \"calculation_currency\": \"USD\",\n \"country\": \"France\",\n \"code\": \"AO32ND56\",\n \"arrival_date\": \"2023-12-01\",\n \"junior\": true\n },\n {\n \"email\": \"user-test-2@company.com\",\n \"name\": \"User 2\",\n \"role\": \"Manager\",\n \"currency\": \"USD\",\n \"calculation_currency\": \"USD\",\n \"country\": \"Germany\",\n \"code\": \"AO32ND57\",\n \"arrival_date\": \"2024-01-01\",\n \"junior\": false\n },\n \"...\"\n ]\n}"
response = http.request(request)
puts response.read_body{
"import_id": "QfejsQZ6mpOVHT5Tt1SdNLPZ",
"objects_imported": 2
}{
"count": 1,
"errors": [
{
"error": "ValidationError",
"resource": "data",
"description": "Too many users, at most 200 can be sent per call"
}
]
}How to use user attributes
If you want to add custom columns to your user table, create them inQobra settings > User attributes first.

Variable api keys
This endpoint is based on dynamic fields. This mean that all annotations that look like<variable-key>, will have to or will be replaced by their
actual API keys.
Asynchronous upsert
This endpoint does not apply your payload during the request: it stores it and creates an import that applies it in the background. The response gives you theimport_id of that import, and the number of items your request contained.
{
"import_id": "QfejsQZ6mpOVHT5Tt1SdNLPZ",
"objects_imported": 2
}
200 therefore means the import was created, not the payload was applied.
To know how it went, fetch the import with the
Fetch import route until its
status is finished, then read its error flag:
error: false— every item of your payload has been upserted.error: true— readtitleindata_tables_details[].errorto know what happened.ValidationErrormeans the payload was refused and nothing was upserted,descriptionlisting the invalid items.PartialFailuremeans part of it was applied:descriptioncounts the items that reported an issue, andparsing_error_detailsnames the skipped ones — the most recent 100 — each with itsline_number,rowanddetails.
scheduled
for a while when another one is already running.
A 400 is still returned during the request itself, for a malformed body or a
payload above the limit — in that case no import is created.
Merging successive calls with debounce
A payload above the limit of 200 has to be split across several calls, and each
call would otherwise create its own import — all queued behind one another.
Pass debounce, a number of seconds, to hold the import open: every call to
the same endpoint made during that window joins the same import instead of
queuing a new one, and they all return the same import_id. The window is
opened by the first call, and a later call joins it without pushing its start
further.
Each payload keeps its own validation. A refused payload upserts nothing, but
the payloads merged beside it are still applied — the import then reports the
reason of every refusal in data_tables_details[].error.
Only calls to the same endpoint are merged: users, manager attributions and
group attributions always get their own import. This is deliberate, since
attributions need the users they reference to exist already — send your users
first, wait for their import to finish, then send the attributions.
Behavior
- Conflict Rule and upsert: Be careful, email field on user is unique. For each user, if it already exist (according to email), we will update the existing one and if it does not exist yet, we will create it.
- Transaction: If there is at least one user that triggers an error at import, none of the users of your call are upserted and the import finishes in error. You have to fix your payload and send it to us again. And if the import finishes without error, it means that every users you sent us have been upserted. This all-or-nothing applies to the payload of your own call: when several calls share an import through
debounce, a refused payload does not prevent the others from being applied - Repeated users: sending the same user twice in one payload does not refuse
the call. The first occurrence is upserted, and each later one is skipped and
reported in
parsing_error_detailswith adetailssuch as"email: duplicated (jane@qobra.co)". Two users are the same when they share the identifier we match on, compared case-insensitively for an email - Empty Field Rule: If you don’t specify a field in upsert, this field won’t be modified. If you want to empty a field, you have to send us a value null associated to the given field api key
- Limit: you can’t upsert more than 200 users at a time.
- Error behavior: a refused payload upserts none of its users, but a user skipped on its own — a repeated user, or one we could not write — leaves all the others upserted. Read the import rather than assuming an all-or-nothing outcome
Authorizations
Query Parameters
Optional (minimum: 0): debounce is the number of seconds we wait before starting the import. Every call to this endpoint made during that window joins the same import instead of queuing a new one, and they all return the same import_id. Use it when a payload above the limit of 200 has to be split across several calls. Only calls to this same endpoint are merged: users, manager attributions and group attributions always get their own import.
Body
The body contains the list of user data necessary to create a new user in Qobra
Show child attributes
Show child attributes
Response
Import created
The upsert is performed as a background import: the response identifies the import you just created, not the upserted objects. Fetch the import to know how it went.
Was this page helpful?