Cobalt External API (1.0.0)

Download OpenAPI specification:

Customer-facing external API exposing legacy Cobalt contacts, users, groups, and incident reports.

  • All routes (except /health) require an x-api-key header.
  • The key is matched against the static map in services/api/src/config/apiKeyTenants.ts and selects the tenant database (same name for MySQL + MongoDB).
  • Success bodies are wrapped in { data: ... }. Errors are returned as { errors: [{ msg: string }] }.
  • Batch endpoints accept up to 30 records per call and return per-record success/failure summaries (HTTP 207).

Health

Liveness probe

Public route — no x-api-key required.

Responses

Response samples

Content type
application/json
{
  • "status": true
}

Contact

Create a contact

Authorizations:
ApiKeyAuth
Request Body schema: application/json
required
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: []

Responses

Request samples

Content type
application/json
{
  • "title": "Mr.",
  • "firstName": "Jane",
  • "lastName": "Doe",
  • "email": "jane.doe@example.com",
  • "phones": [
    ],
  • "groupIds": [ ]
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

List contacts

Authorizations:
ApiKeyAuth
query Parameters
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.

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Get a contact by id

Authorizations:
ApiKeyAuth
path Parameters
contactId
required
integer >= 1

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Update a contact

Authorizations:
ApiKeyAuth
path Parameters
contactId
required
integer >= 1
Request Body schema: application/json
required
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 ]

Responses

Request samples

Content type
application/json
{
  • "title": "string",
  • "firstName": "string",
  • "middleName": "string",
  • "lastName": "string",
  • "secondaryEmail": "user@example.com",
  • "language": "string",
  • "externalId": "string",
  • "phones": [
    ],
  • "groupIds": [
    ]
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

Soft-delete a contact

Order of checks:

  1. If the contact's profile is already soft-deleted → 404.
  2. If a user is still linked to the same profile → 400 (the user must be deleted first).
  3. Otherwise: mark 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.
Authorizations:
ApiKeyAuth
path Parameters
contactId
required
integer >= 1

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Create up to 30 contacts

Authorizations:
ApiKeyAuth
Request Body schema: application/json
required
Array ([ 1 .. 30 ] items)
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: []

Responses

Request samples

Content type
application/json
[
  • {
    }
]

Response samples

Content type
application/json
{
  • "data": {
    }
}

Update up to 30 contacts

Authorizations:
ApiKeyAuth
Request Body schema: application/json
required
Array ([ 1 .. 30 ] items)
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

Responses

Request samples

Content type
application/json
[
  • {
    }
]

Response samples

Content type
application/json
{
  • "data": {
    }
}

Soft-delete up to 30 contacts

Authorizations:
ApiKeyAuth
Request Body schema: application/json
required
ids
required
Array of integers [ 1 .. 30 ] items [ items >= 1 ]

Responses

Request samples

Content type
application/json
{
  • "ids": [
    ]
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

User

Create a user (requires existing contact)

Provisioning flow with pre-checks:

  1. Validates role.id404 if the role doesn't exist, 400 if it's hidden=true (system roles can't be assigned through this API).
  2. Resolves the contact's profile_id404 if contactId doesn't exist.
  3. Enforces 1:1 contact↔user — 400 if any user is already linked to that contact's profile.
  4. Username uniqueness — 400 if users.username is already taken.
  5. Calls the legacy create_user_with_contact stored procedure. 400 if the per-tenant user/admin cap is exceeded (User limit exceeded / Admin limit exceeded).
  6. Provisions a Cognito user in the tenant's User Pool via 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.

Authorizations:
ApiKeyAuth
Request Body schema: application/json
required
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

Responses

Request samples

Content type
application/json
{
  • "username": "jdoe",
  • "active": false,
  • "contactId": 1,
  • "role": {
    }
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

List users

Authorizations:
ApiKeyAuth
query Parameters
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.

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Get a user by id

Authorizations:
ApiKeyAuth
path Parameters
userId
required
integer >= 1

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Update a user (user-fields-only)

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.

Authorizations:
ApiKeyAuth
path Parameters
userId
required
integer >= 1
Request Body schema: application/json
required
active
boolean
policyAgreed
boolean
object

Responses

Request samples

Content type
application/json
{
  • "active": true,
  • "policyAgreed": true,
  • "role": {
    }
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

Soft-delete a user

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.

Authorizations:
ApiKeyAuth
path Parameters
userId
required
integer >= 1

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Create up to 30 users

Authorizations:
ApiKeyAuth
Request Body schema: application/json
required
Array ([ 1 .. 30 ] items)
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

Responses

Request samples

Content type
application/json
[
  • {
    }
]

Response samples

Content type
application/json
{
  • "data": {
    }
}

Update up to 30 users

Authorizations:
ApiKeyAuth
Request Body schema: application/json
required
Array ([ 1 .. 30 ] items)
active
boolean
policyAgreed
boolean
object
id
required
integer >= 1

Responses

Request samples

Content type
application/json
[
  • {
    }
]

Response samples

Content type
application/json
{
  • "data": {
    }
}

Soft-delete up to 30 users

Authorizations:
ApiKeyAuth
Request Body schema: application/json
required
ids
required
Array of integers [ 1 .. 30 ] items [ items >= 1 ]

Responses

Request samples

Content type
application/json
{
  • "ids": [
    ]
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

Group

Create a group

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.

Authorizations:
ApiKeyAuth
Request Body schema: application/json
required
name
required
string non-empty
externalId
string or null

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "externalId": "string"
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

List groups

Authorizations:
ApiKeyAuth
query Parameters
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.

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Get a group by id

Authorizations:
ApiKeyAuth
path Parameters
groupId
required
integer >= 1

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Update a group

Authorizations:
ApiKeyAuth
path Parameters
groupId
required
integer >= 1
Request Body schema: application/json
required
name
string non-empty
externalId
string or null

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "externalId": "string"
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

Delete a group

Cascading FK on groups_contacts removes membership rows automatically.

Authorizations:
ApiKeyAuth
path Parameters
groupId
required
integer >= 1

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Add contacts to a group (idempotent)

Authorizations:
ApiKeyAuth
Request Body schema: application/json
required
groupId
required
integer >= 1
contactIds
required
Array of integers [ 1 .. 100 ] items [ items >= 1 ]

Responses

Request samples

Content type
application/json
{
  • "groupId": 1,
  • "contactIds": [
    ]
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

Remove contacts from a group (bulk, idempotent)

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.

Authorizations:
ApiKeyAuth
Request Body schema: application/json
required
groupId
required
integer >= 1
contactIds
required
Array of integers [ 1 .. 100 ] items [ items >= 1 ]

Responses

Request samples

Content type
application/json
{
  • "groupId": 1,
  • "contactIds": [
    ]
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

List contacts in a group

Authorizations:
ApiKeyAuth
path Parameters
groupId
required
integer >= 1
query Parameters
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.

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Incident

List incident reports

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.

Authorizations:
ApiKeyAuth
query Parameters
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

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}