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.

Accepted credentials
FieldTypeRequiredDescription
API keyheadersOptionalx-auth-method: api-key + x-api-key-id: ak_... + x-api-secret: sk_... — the key must have the portal-users permission.
Session cookiecookieOptionalvinc_b2b_session — set by the B2B portal login flow.

Endpoints

POST/api/b2b/portal-users/import/apiauth: api-key
GET/api/b2b/portal-users/import/api/{job_id}auth: api-key

Import portal users

POST/api/b2b/portal-users/import/apiauth: api-key

Queues 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.length5000 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

FieldTypeRequiredDescription
usersPortalUserImportItem[]RequiredArray of user payloads — see the field table below.
merge_mode'replace' | 'partial'OptionalBoth modes only touch the fields you send; arrays such as customer_access are replaced wholesale when present. Use 'partial' to be explicit.(default: 'replace')
channelstringOptionalDefault channel for users created in this batch. A per-user channel takes priority. Falls back to the tenant default channel.
batch_metadataobjectOptionalOptional { batch_id, batch_part, batch_total_parts, batch_total_items } — persisted on the ImportJob so multi-part feeds can be correlated.

PortalUserImportItem

User fields
FieldTypeRequiredDescription
usernamestringRequiredLogin identifier — normally the email. Lower-cased and trimmed. Used as the upsert key.
emailstringRequiredContact email. Lower-cased and trimmed. Must be unique within the tenant — a clash rejects that row.
passwordstringOptionalPlaintext password; hashed with bcrypt server-side. Required when creating a new user; omit on update to keep the current password.
is_activebooleanOptionalWhether the account can sign in.(default: true)
channelstringOptionalChannel for this user. Overrides the batch-level channel.
customer_accessImportCustomerAccess[]OptionalWhich companies — and which of their addresses — this user may act on. Replaced wholesale when provided. See below.

ImportCustomerAccess

customer_access entry
FieldTypeRequiredDescription
customer_external_codestringOptionalThe company's external_code (from the companies import). Resolved to the internal customer id server-side. Provide this or customer_id.
customer_idstringOptionalThe 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

FieldTypeRequiredDescription
successbooleanOptionaltrue when the batch was accepted and queued.
job_idstringOptionalImportJob id, prefix 'pu_import_'. Poll it for progress and per-row errors.
totalintegerOptionalNumber of users queued.
merge_mode'replace' | 'partial'OptionalEcho of the mode applied.
messagestringOptionalHuman-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 — upsert two users
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
    }
  ]
}'
202 Accepted
{
"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.

One login, two companies
{
"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

GET/api/b2b/portal-users/import/api/{job_id}auth: api-key

Returns the ImportJob created by the POST above. Poll until status is completed or failed.

FieldTypeRequiredDescription
job.job_idstringOptionalEchoes the requested id.
job.status'pending' | 'processing' | 'completed' | 'failed'OptionalLifecycle state.
job.total_rowsintegerOptionalUsers in the batch.
job.processed_rowsintegerOptionalUsers processed so far.
job.successful_rowsintegerOptionalUsers created or updated.
job.failed_rowsintegerOptionalUsers rejected — see import_errors.
job.import_errors{ row, entity_code, error, raw_data }[]OptionalPer-row failures with the offending payload (first 1000).
job.batch_id / batch_part / batch_total_partsstring / integerOptionalEchoed from batch_metadata when supplied.
job.started_at / completed_at / duration_secondsstring / numberOptionalProcessing timing.
curl — poll the job
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..."
200 OK
{
"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.