Portal Users
Create or update the portal logins your B2B customers use to sign in, and
attach each one to the companies (and addresses) it is allowed to operate on.
The endpoint takes an array of user records, validates it, enqueues a
background job, and returns a job_id. Users are matched on username, so
re-sending the same feed updates the existing accounts.
Authentication
Both endpoints accept either a tenant API key or a B2B portal session cookie.
| Field | Type | Required | Description |
|---|---|---|---|
API key | headers | Optional | x-auth-method: api-key + x-api-key-id: ak_... + x-api-secret: sk_... — the key must have the portal-users permission. |
Session cookie | cookie | Optional | vinc_b2b_session — set by the B2B portal login flow. |
Endpoints
/api/b2b/portal-users/import/apiauth: api-key/api/b2b/portal-users/import/api/{job_id}auth: api-keyImport portal users
/api/b2b/portal-users/import/apiauth: api-keyQueues an array of users for upsert. Returns 202 Accepted immediately; the
write happens in a background worker that bcrypt-hashes any plaintext passwords.
Constraints
- 1 ≤
users.length≤ 5000 per request. - Every entry must carry a non-empty
username. - Recommended batch size: ≤ 100 users per call — password hashing is the
bottleneck. Split larger feeds into multiple requests sharing a
batch_metadata.batch_id.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
users | PortalUserImportItem[] | Required | Array of user payloads — see the field table below. |
merge_mode | 'replace' | 'partial' | Optional | Both modes only touch the fields you send; arrays such as customer_access are replaced wholesale when present. Use 'partial' to be explicit.(default: 'replace') |
channel | string | Optional | Default channel for users created in this batch. A per-user channel takes priority. Falls back to the tenant default channel. |
batch_metadata | object | Optional | Optional { batch_id, batch_part, batch_total_parts, batch_total_items } — persisted on the ImportJob so multi-part feeds can be correlated. |
PortalUserImportItem
| Field | Type | Required | Description |
|---|---|---|---|
username | string | Required | Login identifier — normally the email. Lower-cased and trimmed. Used as the upsert key. |
email | string | Required | Contact email. Lower-cased and trimmed. Must be unique within the tenant — a clash rejects that row. |
password | string | Optional | Plaintext password; hashed with bcrypt server-side. Required when creating a new user; omit on update to keep the current password. |
is_active | boolean | Optional | Whether the account can sign in.(default: true) |
channel | string | Optional | Channel for this user. Overrides the batch-level channel. |
customer_access | ImportCustomerAccess[] | Optional | Which companies — and which of their addresses — this user may act on. Replaced wholesale when provided. See below. |
ImportCustomerAccess
| Field | Type | Required | Description |
|---|---|---|---|
customer_external_code | string | Optional | The company's external_code (from the companies import). Resolved to the internal customer id server-side. Provide this or customer_id. |
customer_id | string | Optional | The internal customer id, if you already have it. Takes priority over customer_external_code. |
address_access | 'all' | string[] | Required | 'all' to allow every address of the company, or an array of address external codes / ids to restrict access. |
Response — 202 Accepted
| Field | Type | Required | Description |
|---|---|---|---|
success | boolean | Optional | true when the batch was accepted and queued. |
job_id | string | Optional | ImportJob id, prefix 'pu_import_'. Poll it for progress and per-row errors. |
total | integer | Optional | Number of users queued. |
merge_mode | 'replace' | 'partial' | Optional | Echo of the mode applied. |
message | string | Optional | Human-readable confirmation. |
400 is returned for an empty / oversized users array, an invalid
merge_mode, or a user missing username (the error names the offending index).
Example
curl -X POST https://cs.vendereincloud.it/api/b2b/portal-users/import/api \
-H "Content-Type: application/json" \
-H "x-auth-method: api-key" \
-H "x-api-key-id: ak_acme_live_1234" \
-H "x-api-secret: sk_live_abcdef..." \
-d '{
"merge_mode": "partial",
"channel": "vinc-b2b",
"users": [
{
"username": "info@acme.it",
"email": "info@acme.it",
"password": "S0me-Strong-Secret",
"is_active": true,
"customer_access": [
{ "customer_external_code": "000123", "address_access": "all" },
{ "customer_external_code": "000124", "address_access": ["000000", "000001"] }
]
},
{
"username": "buyer@beta-impianti.it",
"email": "buyer@beta-impianti.it",
"is_active": false
}
]
}'{
"success": true,
"job_id": "pu_import_1714142400000_z9k1m2",
"total": 2,
"merge_mode": "partial",
"message": "Queued 2 portal users for import"
}Linking one user to several companies
Send a single user object with multiple customer_access entries — or several
objects sharing the same email; duplicates are de-duplicated and their
customer_access lists merged before processing.
{
"users": [
{
"username": "agent@group.it",
"email": "agent@group.it",
"password": "init-pass",
"customer_access": [
{ "customer_external_code": "000123", "address_access": "all" },
{ "customer_external_code": "000456", "address_access": "all" }
]
}
]
}Check job status
/api/b2b/portal-users/import/api/{job_id}auth: api-keyReturns the ImportJob created by the POST above. Poll until status is
completed or failed.
| Field | Type | Required | Description |
|---|---|---|---|
job.job_id | string | Optional | Echoes the requested id. |
job.status | 'pending' | 'processing' | 'completed' | 'failed' | Optional | Lifecycle state. |
job.total_rows | integer | Optional | Users in the batch. |
job.processed_rows | integer | Optional | Users processed so far. |
job.successful_rows | integer | Optional | Users created or updated. |
job.failed_rows | integer | Optional | Users rejected — see import_errors. |
job.import_errors | { row, entity_code, error, raw_data }[] | Optional | Per-row failures with the offending payload (first 1000). |
job.batch_id / batch_part / batch_total_parts | string / integer | Optional | Echoed from batch_metadata when supplied. |
job.started_at / completed_at / duration_seconds | string / number | Optional | Processing timing. |
curl https://cs.vendereincloud.it/api/b2b/portal-users/import/api/pu_import_1714142400000_z9k1m2 \
-H "x-auth-method: api-key" \
-H "x-api-key-id: ak_acme_live_1234" \
-H "x-api-secret: sk_live_abcdef..."{
"success": true,
"job": {
"job_id": "pu_import_1714142400000_z9k1m2",
"status": "completed",
"total_rows": 2,
"processed_rows": 2,
"successful_rows": 2,
"failed_rows": 0,
"import_errors": [],
"started_at": "2026-04-24T13:00:00.500Z",
"completed_at": "2026-04-24T13:00:01.300Z",
"duration_seconds": 0.8,
"batch_id": null,
"created_at": "2026-04-24T13:00:00.000Z"
}
}A 404 means no portal_user_import job exists with that id for the
authenticated tenant.