Skill Management APIs

Skill Management API

Create, read, update and delete skill groups programmatically.


Contents


Getting started

Base URL

https://<your-tenant>/rest/api/v1/skills

Replace <your-tenant> with your own hostname. Note the /rest prefix — it is easy to leave out.

Authentication

Every request needs a Service User token:

Authorization: Bearer <your-token>

Create a Service User in the web interface under API Enablement → Service Users, then copy its token.

Any valid Service User token can create, edit and delete skills. There is no separate
permission for skill administration. If you have tokens issued for other integrations, they
can all reach these endpoints. Every change is recorded in the settings audit log against the
Service User that made it.

Content type

All request bodies are JSON. Send Content-Type: application/json.

Your first call

curl -s "https://<your-tenant>/rest/api/v1/skills" \
  -H "Authorization: Bearer $TOKEN"

How responses work

There are two kinds of failure, and telling them apart matters.

Request-level failures → HTTP error status

The whole request was rejected. Nothing was written.

{
  "exception": "ApiConflictException",
  "message": "\"Support\" is this tenant's default skill and cannot be deleted. Make another skill the default first."
}

Item-level failures → HTTP 200, with status: "REJECTED"

The request was understood and processed, but one or more skills in it could not be written.
This still returns 200. A bulk request where three of four skills succeed is a successful
request with one rejected item — not an error.

{
  "results": [
    { "index": 0, "id": "9f2c…", "name": "Sales", "status": "CREATED", "warnings": [], "errors": [] },
    { "index": 1, "name": "Billing", "status": "REJECTED", "warnings": [],
      "errors": [{ "field": "extension", "reason": "Extension 5100 is already used by skill \"Support\"." }] }
  ],
  "summary": { "created": 1, "updated": 0, "deleted": 0, "rejected": 1 }
}

Always check summary.rejected and each result's status. Do not rely on the HTTP status alone.

Warnings

A warning means the write happened, and something in it was adjusted. Every write response
carries a warnings[] array per item.

{
  "field": "voiceSettings.queueMusic",
  "submitted": "hold-music-v2",
  "action": "DEFAULTED",
  "resolvedTo": "snap-clap-whistle.ul",
  "reason": "No queue music with that name exists on this tenant."
}
actionMeaning
DEFAULTEDYour value did not resolve, so the product default was applied. resolvedTo says what.
CLEAREDYour value did not resolve and there is no default, so the setting was left unset. Read the effect field — for a routing destination this disables that routing.
IGNOREDThe field name was not recognised, or an agent entry could not be matched. Nothing was applied for it.

Warnings are how you catch typos. A misspelled field name is not an error — it is silently not
applied — so IGNORED warnings are the only signal that you sent something the API did not
understand. Log them.

Result statuses

CREATED · UPDATED · DELETED · REJECTED

You may notice a rejected: true/false field on each result. It duplicates status and can
be ignored.


Endpoints

MethodPathPurpose
GET/rest/api/v1/skillsList all skills
GET/rest/api/v1/skills/{id}One skill in full
POST/rest/api/v1/skills/createCreate a skill
PATCH/rest/api/v1/skills/{id}Update a skill
PATCH/rest/api/v1/skillsCreate and update many at once
DELETE/rest/api/v1/skills/{id}Delete a skill

Skills are addressed by id (a UUID), not by name. Use the list endpoint to map your own
names to ids.


List skills

GET /rest/api/v1/skills

Response 200

{
  "skills": [
    { "id": "70f9cf55-0963-44b7-88de-cb154f1dc598", "name": "CC Sales", "extension": "2101", "isDefault": false },
    { "id": "28c4f023-a442-4f35-9a63-d65d82d6901d", "name": "CC Support", "extension": "2102", "isDefault": true }
  ]
}

Sorted by name. extension is omitted for a skill that has none.

StatusMeaning
200Success
401Missing or invalid token

Get one skill

GET /rest/api/v1/skills/{id}

Response 200 (settings abbreviated)

{
  "id": "70f9cf55-0963-44b7-88de-cb154f1dc598",
  "name": "CC Sales",
  "extension": "2101",
  "isDefault": false,
  "settings": {
    "skillName": "CC Sales",
    "extension": "2101",
    "aliasNumbers": [],
    "priority": 10,
    "voiceSettings": { "routingAlgorithm": "LINEAR", "agentRingTimeSeconds": 30 },
    "callbackSettings": { "callbackStrategy": "WAIT_IN_QUEUE" },
    "messagingSettings": { "inviteHeaderText": "Tell us about yourself" }
  },
  "agents": [
    {
      "agent": "Agent Kelly(556)",
      "name": "Agent Kelly",
      "extension": "556",
      "skillLevel": 10,
      "maxSimultaneousSessions": 2,
      "voiceEnabled": true,
      "chatEnabled": true,
      "emailEnabled": true
    }
  ]
}

The settings object is the same shape the write endpoints accept, so you can fetch a
skill, change a value, and send it back.

agents lists only agents who are both assigned to the skill (skill level above 0) and
currently licensed. An agent who still holds a stored skill level but has lost their
licence is not listed, because they cannot take interactions.

StatusMeaning
200Success
401Missing or invalid token
404No skill with that id

Create a skill

POST /rest/api/v1/skills/create

Required: skillName only. Everything else takes a product default.

Request

{
  "skillName": "Customer Care",
  "extension": "2091",
  "priority": 10,
  "voiceSettings": {
    "routingAlgorithm": "LINEAR",
    "agentRingTimeSeconds": 30
  },
  "callbackSettings": {
    "callbackStrategy": "WAIT_IN_QUEUE"
  },
  "agents": [
    { "agent": "Agent Kelly(556)", "skillLevel": 10 }
  ]
}

Response 200

{
  "results": [
    { "index": 0, "id": "7f4fa573-1acc-413c-9676-d380ee3f495f", "name": "Customer Care",
      "status": "CREATED", "warnings": [], "errors": [] }
  ],
  "summary": { "created": 1, "updated": 0, "deleted": 0, "rejected": 1 }
}

The new skill's id is in results[0].id. Store it.

Item rejected when: skillName is missing, the name is already in use, the extension is
already in use, or the body cannot be read (for example a value that is not a valid choice for
its field). These come back as 200 with status: "REJECTED".

StatusMeaning
200Processed. Check status — may still be REJECTED
400No body supplied, or the agents list could not be read
401Missing or invalid token
429Another write is in progress. Retry

Update a skill

PATCH /rest/api/v1/skills/{id}

Required: nothing. Send only what you are changing.

Request — turning callbacks off and nothing else:

{ "callbackSettings": { "callbackStrategy": "NO_QCB" } }

Every other callback setting, and all voice and messaging settings, are left exactly as they
were. See Partial updates.

Response 200

{
  "results": [
    { "index": 0, "id": "70f9cf55-0963-44b7-88de-cb154f1dc598", "name": "CC Sales",
      "status": "UPDATED", "warnings": [], "errors": [] }
  ],
  "summary": { "created": 0, "updated": 1, "deleted": 0, "rejected": 0 }
}

Item rejected when: the id does not exist, you attempt a rename, the extension clashes with
another skill, or the body cannot be read.

StatusMeaning
200Processed. Check status
400No body supplied, or the agents list could not be read
401Missing or invalid token
429Another write is in progress. Retry

Create and update many skills

PATCH /rest/api/v1/skills

Use this for anything involving more than a couple of skills. It is not just fewer round
trips: each write triggers a full configuration save and, on Netsapiens tenants, a complete
dial-plan reconciliation. A bulk request does that once regardless of how many skills it
carries. Five hundred single-skill calls will do it five hundred times.

Request — one create, one update:

{
  "skills": [
    { "settings": { "skillName": "Overflow Team", "extension": "2400" } },
    { "id": "70f9cf55-0963-44b7-88de-cb154f1dc598", "settings": { "priority": 5 } }
  ]
}

Each item: id present means update, id absent means create. Agents go alongside
settings:

{ "id": "70f9cf55-…", "settings": { "priority": 5 }, "agents": [ { "agent": "Agent Kelly(556)", "skillLevel": 8 } ] }

Response 200 — one result per item, in the order you sent them (index matches):

{
  "results": [
    { "index": 0, "id": "aa11…", "name": "Overflow Team", "status": "CREATED", "warnings": [], "errors": [] },
    { "index": 1, "id": "70f9cf55-…", "name": "CC Sales", "status": "UPDATED", "warnings": [], "errors": [] }
  ],
  "summary": { "created": 1, "updated": 1, "deleted": 0, "rejected": 0 }
}

A failing item does not stop the others. One bad extension in item 300 of 500 does not
discard the other 499.

StatusMeaning
200Processed. Check each status and summary.rejected
400No skills supplied, or more than 500 items
401Missing or invalid token
429Another write is in progress. Retry

Delete a skill

DELETE /rest/api/v1/skills/{id}

Required: the skill's extension in the body, as confirmation.

{ "extension": "2101" }

For a skill with no extension, confirm its name instead:

{ "name": "Overflow Team" }

Response 200

{
  "results": [
    { "index": 0, "id": "70f9cf55-…", "name": "CC Sales", "status": "DELETED", "warnings": [], "errors": [] }
  ],
  "summary": { "created": 0, "updated": 0, "deleted": 1, "rejected": 0 }
}

Deletion is refused with 409 when:

  • It is your tenant's default skill. Make another skill the default first.
  • The skill currently has calls in queue.
  • Another skill routes to it — via queue timeout, queue overflow, max calls in queue, or
    end-of-day overflow. The message names every referring skill and which route. Clear those
    destinations first.

The last guard exists because those references are by name. Deleting a skill another skill
overflows into does not raise an error at call time — the overflow simply never fires, and
callers wait in the referring skill's queue as though nothing were configured.

StatusMeaning
200Deleted
400No body, or the confirmation does not match the skill
401Missing or invalid token
404No skill with that id
409Blocked by a guard — see the message
429Another write is in progress. Retry

Deleting a skill affects historical reporting. Skills are recorded by name on completed
interactions, so reports filtered on a deleted skill's name keep their history but gain no
new data. This cannot be undone by recreating a skill with the same name — though that does
restore future reporting.


Skill settings reference

All of these live inside the settings object (the request body for single-skill writes, or
settings for bulk items). All are optional.

Top level

FieldTypeNotes
skillNamestringRequired on create. Must be unique. Cannot be changed
extensionstringMust be unique. Can be changed
aliasNumbersstring[]Additional numbers or extensions routing to this skill
priorityintegerHigher wins when an agent serves several skills
recordingModeenumUNDEFINED · AUTOMATIC · AUTOMATIC_PAUSING_PROHIBITED · MANUAL · NEVER. Omit to defer to each agent's own mode

voiceSettings

FieldTypeNotes
routingAlgorithmenumMOST_IDLE · CIRCULAR · LINEAR · HIGHEST_SKILL · NO_ROUTING
availabilityTiersobject{ "tiers": [] } — empty offers to all eligible agents at once
languagePackIdstringLanguage for system prompts
queueAnnouncementsobjectSee below
queueMusicstringHold music name
queueTimeoutobjecttimeoutMillis plus a destination
queueOverflowobjectoverflowCondition (NEVER · NO_AGENTS_ENABLED · NO_AGENTS_READY), checkFrequencySeconds, plus a destination
maxCallsInQueueobjectmaxAllowed plus a destination. 0 means uncapped
clearCallsAtEndOfDayobjectendOfDayInSeconds; 0 disables
digitActionsobjectKeypress options while queued
webrtcAutoAnswerbooleanConnect browser agents without them answering
agentRingTimeSecondsintegerHow long one agent rings before moving on
transcriptLanguageenumENGLISH_US · ENGLISH_UK · ENGLISH_AU · FRENCH · FRENCH_CANADIAN · SPANISH · ITALIAN · DUTCH · DENMARK_DANISH · CHINESE_MANDARIN · CHINESE_CANTONESE
skillVoicemailobjectenabled, greeting (audio id), emailAddresses

Destinations. queueTimeout, queueOverflow, maxCallsInQueue and
clearCallsAtEndOfDay all take destination plus destinationType:

destinationTypedestination holds
SKILLAnother skill's name (not its id)
NUMBERA phone number or extension
IVRAn IVR call flow name
SKILL_VOICEMAILNothing — this skill's own voicemail. Requires skillVoicemail.enabled

Queue announcements.

"queueAnnouncements": {
  "announcements": [
    {
      "startTimeSeconds": 5,
      "repeatIntervalSeconds": 90,
      "repeatLimit": 2147483647,
      "announcementName": "Position in Queue",
      "isCallbackRelated": false
    }
  ],
  "endOfQueueAnnouncement": "This call is recorded"
}

repeatLimit of 2147483647 means repeat indefinitely. isCallbackRelated: true plays the
announcement only when a callback is being offered.

Digit actions are keyed by the digit pressed:

"digitActions": {
  "digitActions": {
    "1": { "destination": "<Queued Callback Module>", "destinationType": "NUMBER" }
  }
}

<Queued Callback Module> is a reserved token, not a real number — it hands the caller to the
queued-callback flow.

callbackSettings

FieldTypeNotes
callbackStrategyenumNO_QCB (off) · WAIT_IN_QUEUE · RESERVE_AGENT
callbackSnoozeSecondsintegerWait before retrying a failed attempt
outboundDialCodestringPrefix when dialling out, e.g. "9"
callbackOfferWindowobjectstartSecondOfDay / endSecondOfDay. 086400 is all day
callbackAttemptWindowobjectSame shape
maxPendingCallbacksinteger
clearPendingCallbacksenumNEVER · NIGHTLY · MIDNIGHT · AM_1AM_11 · PM_12 · PM_1PM_11
maxCallbackAttemptsinteger0 is unlimited
attemptToGuessNumberbooleanInfer the callback number from caller ID
includeEstimatedWaitTimebooleanSpeak the wait estimate when offering

messagingSettings

FieldTypeNotes
inviteHeaderTextstring
whenAllAgentsBusyobjectoption: HIDE · OFFER_EMAIL · OFFER_QUEUE, plus titleText, emailAddress
whenNoAgentsLoggedInobjectoption: HIDE · OFFER_EMAIL (no queue option — there is nobody to queue for)
cannedMessagesarraytitle, body, isInitialMessage. $customerName and $agentName are substituted at send time. Keep at most one isInitialMessage: true
customChatTextobject{ "customizations": {} } — chat widget text overrides
agentResponseTimeoutSecondsintegerIdle agent before the chat is reclaimed
externalPartyTimeoutSecondsintegerIdle customer before the chat closes
autoAnswerboolean

Agent membership

Send an agents array on create or update to manage who is in a skill.

"agents": [
  { "agent": "Agent Kelly(556)", "skillLevel": 10, "maxSimultaneousSessions": 2 },
  { "name": "Randy Ross", "extension": "1990", "skillLevel": 5 },
  { "agent": "Agent Jon(557)", "skillLevel": 0 }
]
FieldNotes
agentThe name(extension) key exactly as GET /skills/{id} returns it. The unambiguous option
name + extensionAlternative to agent. Use both together
skillLevel110 to assign, 0 to remove. Required when adding an agent who is not already in the skill
maxSimultaneousSessionsConcurrent interactions
voiceEnabled / chatEnabled / emailEnabledPer-channel participation

Three rules

1. Omitting agents changes nothing. Membership is left exactly as it was.

2. Supplying agents never removes anyone. Listed agents are added or updated; agents
already in the skill and not listed are untouched. To remove an agent, send them with
skillLevel: 0.
This means a partial or filtered list can never silently strip a skill.

3. Ambiguous agents are skipped, not guessed. A name or extension on its own is often
not unique — several agents can share a name, and two agents can share an extension. If your
value matches more than one agent the entry is skipped with an IGNORED warning listing the
candidates, and you should re-send it using agent, or name and extension together.

Unrecognised or unlicensed agents are also skipped with a warning. The rest of the skill still
writes.


Behaviour you need to understand

Partial updates (merge semantics)

On PATCH, only the fields present in your body change. Everything omitted keeps its
current value, at every level of nesting. This:

{ "voiceSettings": { "agentRingTimeSeconds": 20 } }

changes the ring time and leaves the routing algorithm, queue music, overflow, timeout and
every other voice setting alone.

To clear an optional setting, send it explicitly as null:

{ "voiceSettings": { "queueOverflow": null } }

Omitting a field means "leave it alone". Only an explicit null removes something.

Lists and maps replace, they do not merge. Sending aliasNumbers: ["5101"] sets the list
to exactly that. Same for cannedMessages and customChatText.customizations.

On POST .../create, the same rule applies against product defaults: anything you omit is
created with its default value.

Renaming is not supported

A PATCH whose skillName differs from the skill's current name is rejected (200 with
status: "REJECTED"). Other fields in the same request still apply.

This is deliberate. Skills are referenced by name in several places, including on completed
interactions already stored. A rename would break routing that points at the old name, and
would split reporting history at the date it happened in a way that cannot be repaired. If you
need a different name, create a new skill and move membership across.

Extensions can be changed freely, subject to uniqueness.

Unrecognised references are defaulted, not rejected

If you send an audio name, language pack, caller ID or skill destination that does not exist,
the API applies a sensible fallback and reports it in warnings[] rather than failing your
request. A single stale audio id will never kill a bulk load of hundreds of skills.

The important case is a SKILL destination naming a skill that does not exist. It is
cleared, which disables that routing entirely — no overflow, no timeout. The warning says
so in its effect field. These are the warnings to act on.

Note that a value which is not a valid choice for its field (a misspelled enum, say) is
different: that is malformed input with no sensible fallback, so the item is rejected.

Order does not matter in a bulk request

Skill destinations are resolved after every skill in the request has been applied, so this
works even though Ref A names a skill that does not exist yet when it is processed:

{
  "skills": [
    { "settings": { "skillName": "Ref A", "extension": "5001",
      "voiceSettings": { "queueOverflow": { "overflowCondition": "NO_AGENTS_ENABLED",
        "destinationType": "SKILL", "destination": "Ref B", "checkFrequencySeconds": 5 } } } },
    { "settings": { "skillName": "Ref B", "extension": "5002" } }
  ]
}

You do not need to sort your skills so that referenced skills come first.

One write at a time

Skill writes are serialised. A write arriving while another is in progress gets 429
rather than being queued. Retry on 429 — a short backoff is enough. Do not run parallel
write requests; you will simply get 429s and no extra throughput.

Reads are not affected.


Building an integration

Map your names to ids once

Skills are addressed by id, but your own system probably keys on names or extensions. Fetch the
list once and cache the mapping:

curl -s "$BASE/rest/api/v1/skills" -H "Authorization: Bearer $TOKEN" \
  | jq 'reduce .skills[] as $s ({}; .[$s.name] = $s.id)'

Refresh it after any create.

Read, change, write back

The settings object from GET /skills/{id} is exactly what the write endpoints accept:

curl -s "$BASE/rest/api/v1/skills/$ID" -H "Authorization: Bearer $TOKEN" \
  | jq '{settings: .settings, agents: .agents}' > skill.json
# edit skill.json, then send just the settings object plus agents

In practice you rarely need this — sending only the fields you are changing is smaller, safer
and less likely to trip the rename guard.

Bulk load in batches of 500

for each batch of up to 500 skills:
    PATCH /rest/api/v1/skills  with {"skills": [...]}
    on 429 -> back off briefly and retry the same batch
    for each result:
        if status == REJECTED -> log result.errors, flag for a human
        if warnings non-empty -> log them

Do not send batches in parallel. One at a time, sequentially.

Handle every response properly

if HTTP >= 400:
    the whole request failed; read .message
else:
    for each result:
        REJECTED -> your data needs fixing, see .errors
        otherwise -> written. Check .warnings:
            DEFAULTED -> a reference did not resolve; a default was used
            CLEARED   -> a setting was removed; read .effect
            IGNORED   -> a field name or agent was not recognised — probably a typo

Treating IGNORED warnings as harmless is the most likely way to have a setting silently not
apply.


Limits and known gaps

Bulk size500 skills per request. More returns 400
ConcurrencyOne write at a time across the whole tenant. Others get 429
RenameNot supported
Agent removalRequires an explicit skillLevel: 0

Voicemail greeting ids are not validated. An invalid skillVoicemail.greeting is stored
without a warning. Verify by listening to the skill's greeting after a write.

IVR references are not checked on delete. A skill that only an IVR call flow routes to can
be deleted without a 409. Check IVR call flows yourself before deleting a skill.

Web interface edits are not coordinated with the API. If an administrator saves a skill in
the web interface while your integration is writing, one of the two changes can be lost. Avoid
running bulk loads while administrators are working in the Skills panel.

Unlicensed agents keep their stored membership. They are not returned by
GET /skills/{id} and cannot be written, but their skill levels remain and reappear if the
agent is licensed again.