Download OpenAPI specification:
Customer-facing external API exposing legacy Cobalt contacts, users, groups, and incident reports.
/health) require an x-api-key header.services/api/src/config/apiKeyTenants.ts and selects the tenant database (same name for MySQL + MongoDB).{ data: ... }. Errors are returned as { errors: [{ msg: string }] }.| title | string |
| firstName required | string non-empty |
| middleName | string |
| lastName required | string non-empty |
| email required | string <email> |
| secondaryEmail | string <email> |
| language | string |
| externalId | string |
Array of objects (PhoneInput) <= 5 items Default: [] | |
| groupIds | Array of integers[ items >= 1 ] Default: [] |
{- "title": "Mr.",
- "firstName": "Jane",
- "lastName": "Doe",
- "email": "jane.doe@example.com",
- "phones": [
- {
- "typeId": 1,
- "prefixId": 1,
- "number": "5551234567"
}
], - "groupIds": [ ]
}{- "data": {
- "id": 0,
- "external_id": "string",
- "profile": {
- "id": 0,
- "title": "string",
- "firstName": "string",
- "middleName": "string",
- "lastName": "string",
- "email": "user@example.com",
- "secondaryEmail": "user@example.com",
- "language": "string",
- "picture": "string"
}, - "phones": [
- {
- "id": 0,
- "number": "string",
- "extension": "string",
- "prefix": {
- "id": 0,
- "country": "string",
- "code": "string"
}, - "type": {
- "id": 0,
- "name": "string"
}
}
], - "groups": [
- {
- "id": 0,
- "name": "string"
}
], - "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
}| page | integer >= 0 Default: 0 Zero-indexed page number. |
| size | integer [ 1 .. 100 ] Default: 20 Page size. |
| search | string Substring match across the resource's display fields. |
| ids | string Comma-separated list of ids to include. |
| exceptIds | string Comma-separated list of ids to exclude. |
{- "data": {
- "total": 0,
- "limit": 0,
- "offset": 0,
- "items": [
- {
- "id": 0,
- "external_id": "string",
- "profile": {
- "id": 0,
- "title": "string",
- "firstName": "string",
- "middleName": "string",
- "lastName": "string",
- "email": "user@example.com",
- "secondaryEmail": "user@example.com",
- "language": "string",
- "picture": "string"
}, - "phones": [
- {
- "id": 0,
- "number": "string",
- "extension": "string",
- "prefix": {
- "id": 0,
- "country": "string",
- "code": "string"
}, - "type": {
- "id": 0,
- "name": "string"
}
}
], - "groups": [
- {
- "id": 0,
- "name": "string"
}
], - "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
]
}
}{- "data": {
- "id": 0,
- "external_id": "string",
- "profile": {
- "id": 0,
- "title": "string",
- "firstName": "string",
- "middleName": "string",
- "lastName": "string",
- "email": "user@example.com",
- "secondaryEmail": "user@example.com",
- "language": "string",
- "picture": "string"
}, - "phones": [
- {
- "id": 0,
- "number": "string",
- "extension": "string",
- "prefix": {
- "id": 0,
- "country": "string",
- "code": "string"
}, - "type": {
- "id": 0,
- "name": "string"
}
}
], - "groups": [
- {
- "id": 0,
- "name": "string"
}
], - "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
}| contactId required | integer >= 1 |
| title | string |
| firstName | string non-empty |
| middleName | string |
| lastName | string non-empty |
| secondaryEmail | string <email> |
| language | string |
| externalId | string |
Array of objects (PhoneInput) <= 5 items | |
| groupIds | Array of integers[ items >= 1 ] |
{- "title": "string",
- "firstName": "string",
- "middleName": "string",
- "lastName": "string",
- "secondaryEmail": "user@example.com",
- "language": "string",
- "externalId": "string",
- "phones": [
- {
- "typeId": 1,
- "prefixId": 1,
- "number": "string",
- "extension": "string"
}
], - "groupIds": [
- 1
]
}{- "data": {
- "id": 0,
- "external_id": "string",
- "profile": {
- "id": 0,
- "title": "string",
- "firstName": "string",
- "middleName": "string",
- "lastName": "string",
- "email": "user@example.com",
- "secondaryEmail": "user@example.com",
- "language": "string",
- "picture": "string"
}, - "phones": [
- {
- "id": 0,
- "number": "string",
- "extension": "string",
- "prefix": {
- "id": 0,
- "country": "string",
- "code": "string"
}, - "type": {
- "id": 0,
- "name": "string"
}
}
], - "groups": [
- {
- "id": 0,
- "name": "string"
}
], - "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
}Order of checks:
profiles.deleted=true, null external_id, recycle the email with a UUID prefix (<uuid>:<email>) so the address can be reused, and drop all groups_contacts rows for this contact.| contactId required | integer >= 1 |
{- "data": {
- "deleted": true,
- "id": 0
}
}| title | string |
| firstName required | string non-empty |
| middleName | string |
| lastName required | string non-empty |
| email required | string <email> |
| secondaryEmail | string <email> |
| language | string |
| externalId | string |
Array of objects (PhoneInput) <= 5 items Default: [] | |
| groupIds | Array of integers[ items >= 1 ] Default: [] |
[- {
- "title": "Mr.",
- "firstName": "Jane",
- "lastName": "Doe",
- "email": "jane.doe@example.com",
- "phones": [
- {
- "typeId": 1,
- "prefixId": 1,
- "number": "5551234567"
}
], - "groupIds": [ ]
}
]{- "data": {
- "summary": {
- "total": 0,
- "succeeded": 0,
- "failed": 0
}, - "results": [
- {
- "index": 0,
- "success": true,
- "data": null,
- "error": {
- "msg": "string",
- "code": "string"
}
}
]
}
}| title | string |
| firstName | string non-empty |
| middleName | string |
| lastName | string non-empty |
| secondaryEmail | string <email> |
| language | string |
| externalId | string |
Array of objects (PhoneInput) <= 5 items | |
| groupIds | Array of integers[ items >= 1 ] |
| id required | integer >= 1 |
[- {
- "title": "string",
- "firstName": "string",
- "middleName": "string",
- "lastName": "string",
- "secondaryEmail": "user@example.com",
- "language": "string",
- "externalId": "string",
- "phones": [
- {
- "typeId": 1,
- "prefixId": 1,
- "number": "string",
- "extension": "string"
}
], - "groupIds": [
- 1
], - "id": 1
}
]{- "data": {
- "summary": {
- "total": 0,
- "succeeded": 0,
- "failed": 0
}, - "results": [
- {
- "index": 0,
- "success": true,
- "data": null,
- "error": {
- "msg": "string",
- "code": "string"
}
}
]
}
}| ids required | Array of integers [ 1 .. 30 ] items [ items >= 1 ] |
{- "ids": [
- 1,
- 2,
- 3
]
}{- "data": {
- "summary": {
- "total": 0,
- "succeeded": 0,
- "failed": 0
}, - "results": [
- {
- "index": 0,
- "success": true,
- "data": null,
- "error": {
- "msg": "string",
- "code": "string"
}
}
]
}
}Provisioning flow with pre-checks:
role.id — 404 if the role doesn't exist, 400 if it's hidden=true (system roles can't be assigned through this API).profile_id — 404 if contactId doesn't exist.users.username is already taken.create_user_with_contact stored procedure. 400 if the per-tenant user/admin cap is exceeded (User limit exceeded / Admin limit exceeded).AdminCreateUser with MessageAction: SUPPRESS (no welcome email). Cognito failures are logged but do not roll back the MySQL row.No password is accepted — Cognito owns authentication. The Cognito user starts in FORCE_CHANGE_PASSWORD state and goes through the standard reset-password flow on first login.
| username required | string non-empty |
| active | boolean Default: false |
| contactId required | integer >= 1 Existing contact id. The underlying profile_id is resolved from this. |
required | object |
{- "username": "jdoe",
- "active": false,
- "contactId": 1,
- "role": {
- "id": 1
}
}{- "data": {
- "id": 0,
- "username": "string",
- "active": true,
- "policyAgreed": true,
- "lastLogin": "2019-08-24T14:15:22Z",
- "lastPasswordResetDate": "2019-08-24T14:15:22Z",
- "profile": {
- "id": 0,
- "title": "string",
- "firstName": "string",
- "middleName": "string",
- "lastName": "string",
- "email": "user@example.com",
- "secondaryEmail": "user@example.com",
- "language": "string",
- "picture": "string"
}, - "role": {
- "id": 0,
- "name": "string",
- "hidden": true
}, - "contact": {
- "id": 0
}, - "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
}| page | integer >= 0 Default: 0 Zero-indexed page number. |
| size | integer [ 1 .. 100 ] Default: 20 Page size. |
| search | string Substring match across the resource's display fields. |
| ids | string Comma-separated list of ids to include. |
| exceptIds | string Comma-separated list of ids to exclude. |
{- "data": {
- "total": 0,
- "limit": 0,
- "offset": 0,
- "items": [
- {
- "id": 0,
- "username": "string",
- "active": true,
- "policyAgreed": true,
- "lastLogin": "2019-08-24T14:15:22Z",
- "lastPasswordResetDate": "2019-08-24T14:15:22Z",
- "profile": {
- "id": 0,
- "title": "string",
- "firstName": "string",
- "middleName": "string",
- "lastName": "string",
- "email": "user@example.com",
- "secondaryEmail": "user@example.com",
- "language": "string",
- "picture": "string"
}, - "role": {
- "id": 0,
- "name": "string",
- "hidden": true
}, - "contact": {
- "id": 0
}, - "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
]
}
}{- "data": {
- "id": 0,
- "username": "string",
- "active": true,
- "policyAgreed": true,
- "lastLogin": "2019-08-24T14:15:22Z",
- "lastPasswordResetDate": "2019-08-24T14:15:22Z",
- "profile": {
- "id": 0,
- "title": "string",
- "firstName": "string",
- "middleName": "string",
- "lastName": "string",
- "email": "user@example.com",
- "secondaryEmail": "user@example.com",
- "language": "string",
- "picture": "string"
}, - "role": {
- "id": 0,
- "name": "string",
- "hidden": true
}, - "contact": {
- "id": 0
}, - "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
}Calls update_user(...) for role/active (password is always NULL — Cognito owns auth). policyAgreed is patched directly. Profile fields belong to the contact resource and aren't accepted here.
Hidden-role guard: rejects with 400 if the target user's CURRENT role is hidden=true (system user — not mutable through this API), and rejects with 400 if the requested new role.id is hidden=true or 404 if it doesn't exist.
| userId required | integer >= 1 |
| active | boolean |
| policyAgreed | boolean |
object |
{- "active": true,
- "policyAgreed": true,
- "role": {
- "id": 1
}
}{- "data": {
- "id": 0,
- "username": "string",
- "active": true,
- "policyAgreed": true,
- "lastLogin": "2019-08-24T14:15:22Z",
- "lastPasswordResetDate": "2019-08-24T14:15:22Z",
- "profile": {
- "id": 0,
- "title": "string",
- "firstName": "string",
- "middleName": "string",
- "lastName": "string",
- "email": "user@example.com",
- "secondaryEmail": "user@example.com",
- "language": "string",
- "picture": "string"
}, - "role": {
- "id": 0,
- "name": "string",
- "hidden": true
}, - "contact": {
- "id": 0
}, - "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
}Hidden-role guard: rejects with 400 if the target user's role is hidden=true (system user — not deletable through this API).
Otherwise soft-deletes the legacy MySQL user (mark profiles.deleted=true, recycle email with a UUID prefix, drop the role link, hard-delete the linked security_agents row, prefix users.username with <uuid>:). After the MySQL transaction commits, calls AdminDeleteUser against the tenant's Cognito User Pool. If a SAML-linked variant saml_<email> exists it is also deleted. UserNotFoundException is treated as success.
Returns the original username and userEmail for downstream cleanup; the caller does not need to do their own Cognito teardown.
| userId required | integer >= 1 |
{- "data": {
- "deleted": true,
- "id": 0,
- "contactId": 0,
- "username": "string",
- "userEmail": "string"
}
}| username required | string non-empty |
| active | boolean Default: false |
| contactId required | integer >= 1 Existing contact id. The underlying profile_id is resolved from this. |
required | object |
[- {
- "username": "jdoe",
- "active": false,
- "contactId": 1,
- "role": {
- "id": 1
}
}
]{- "data": {
- "summary": {
- "total": 0,
- "succeeded": 0,
- "failed": 0
}, - "results": [
- {
- "index": 0,
- "success": true,
- "data": null,
- "error": {
- "msg": "string",
- "code": "string"
}
}
]
}
}| active | boolean |
| policyAgreed | boolean |
object | |
| id required | integer >= 1 |
[- {
- "active": true,
- "policyAgreed": true,
- "role": {
- "id": 1
}, - "id": 1
}
]{- "data": {
- "summary": {
- "total": 0,
- "succeeded": 0,
- "failed": 0
}, - "results": [
- {
- "index": 0,
- "success": true,
- "data": null,
- "error": {
- "msg": "string",
- "code": "string"
}
}
]
}
}| ids required | Array of integers [ 1 .. 30 ] items [ items >= 1 ] |
{- "ids": [
- 1,
- 2,
- 3
]
}{- "data": {
- "summary": {
- "total": 0,
- "succeeded": 0,
- "failed": 0
}, - "results": [
- {
- "index": 0,
- "success": true,
- "data": null,
- "error": {
- "msg": "string",
- "code": "string"
}
}
]
}
}Inserts the group, then upserts the Mongo timestamps document { module: "mobileplan", lastModified: now } so mobile clients polling that key know to re-sync their group cache. The timestamp bump is non-fatal — a Mongo blip won't fail the create.
| name required | string non-empty |
| externalId | string or null |
{- "name": "string",
- "externalId": "string"
}{- "data": {
- "id": 0,
- "name": "string",
- "externalId": "string"
}
}| page | integer >= 0 Default: 0 Zero-indexed page number. |
| size | integer [ 1 .. 100 ] Default: 20 Page size. |
| search | string Substring match across the resource's display fields. |
| ids | string Comma-separated list of ids to include. |
| exceptIds | string Comma-separated list of ids to exclude. |
{- "data": {
- "total": 0,
- "limit": 0,
- "offset": 0,
- "items": [
- {
- "id": 0,
- "name": "string",
- "externalId": "string"
}
]
}
}| groupId required | integer >= 1 |
| name | string non-empty |
| externalId | string or null |
{- "name": "string",
- "externalId": "string"
}{- "data": {
- "id": 0,
- "name": "string",
- "externalId": "string"
}
}| groupId required | integer >= 1 |
| contactIds required | Array of integers [ 1 .. 100 ] items [ items >= 1 ] |
{- "groupId": 1,
- "contactIds": [
- 1,
- 2,
- 3
]
}{- "data": {
- "groupId": 0,
- "results": [
- {
- "contactId": 0,
- "added": true,
- "alreadyMember": true,
- "error": "string"
}
]
}
}Symmetrical with POST /group/addContact. Sends { groupId, contactIds: [...] } in the body and receives a per-contact result. removed: false means the contact wasn't a member of the group — not an error.
| groupId required | integer >= 1 |
| contactIds required | Array of integers [ 1 .. 100 ] items [ items >= 1 ] |
{- "groupId": 1,
- "contactIds": [
- 1,
- 2,
- 3
]
}{- "data": {
- "groupId": 0,
- "results": [
- {
- "contactId": 0,
- "removed": true
}
]
}
}| groupId required | integer >= 1 |
| page | integer >= 0 Default: 0 Zero-indexed page number. |
| size | integer [ 1 .. 100 ] Default: 20 Page size. |
| search | string Substring match across the resource's display fields. |
| ids | string Comma-separated list of ids to include. |
| exceptIds | string Comma-separated list of ids to exclude. |
{- "data": {
- "total": 0,
- "limit": 0,
- "offset": 0,
- "items": [
- {
- "id": 0,
- "firstName": "string",
- "lastName": "string",
- "email": "user@example.com"
}
]
}
}Returns a normalized projection of legacy runtime-incidents.
Default excludes states ON_GOING, ALERT, MERGED, ARCHIVED. When type=ARCHIVED the exclusion flips to ON_GOING, ALERT, MERGED, CLOSED, IGNORED, COMPLETED so only archived rows surface.
declaredBy resolves from the runtime incident's declaredContactDetails snapshot first, falling back to a MySQL contact lookup.
| search | string Substring match (case-insensitive) on incident name. |
| contactIds | string Comma-separated declaredContactId values to include. |
| type | string Enum: "DEFAULT" "ARCHIVED" Set to ARCHIVED to surface archived incidents. |
| startDate | string <date-time> ISO timestamp — inclusive lower bound on created_at. |
| endDate | string <date-time> ISO timestamp — inclusive upper bound on updated_at. |
| sortBy | string Default: "updated_at" Enum: "created_at" "updated_at" "name" |
| sortOrder | string Default: "desc" Enum: "asc" "desc" |
| limit | integer [ 1 .. 500 ] Default: 100 |
| offset | integer >= 0 Default: 0 |
{- "data": {
- "items": [
- {
- "id": "060426-1775-51824-7872",
- "event": "Bris d'équipement",
- "status": "COMPLETED",
- "declaredBy": "Reyhaneh Tavakolipour",
- "startDate": "2026-04-06T16:30:00.000Z",
- "endDate": "2026-04-07T08:40:00.000Z"
}
], - "total": 1,
- "limit": 100,
- "offset": 0
}
}