SCIM custom integration
This guide is for developers building a custom SCIM client against Agility Credit — for example, an in-house IdP, a middleware that bridges a non-standard system, or automated provisioning scripts. If you're integrating with Okta or Entra ID, use the IdP-specific instructions in SCIM provisioning instead.
Agility's SCIM endpoint implements the SCIM 2.0 protocol with the same operation surface that Okta's provisioning client uses. Any client that speaks Okta-style SCIM 2.0 will work; this page documents the exact contract.
Prerequisites
- An Agility tenant with SCIM provisioning enabled (Settings → Account → Identity → SCIM).
- An Agility API key, created in Settings → Account → API Keys. The key is the SCIM bearer token.
- Your tenant ID (visible in the SCIM endpoint URL shown in the portal).
Base URL and authentication
| Environment | Base URL |
|---|---|
| Production | https://api.agilitycredit.net/scim/v2/<tenant-id> |
| UAT | https://api.agc-dev.com/scim/v2/<tenant-id> |
All requests must include:
Authorization: Bearer <api-key>
Content-Type: application/scim+json
Accept: application/scim+json
Auth failures return 401 without a SCIM error body. When SCIM is disabled on the tenant, every endpoint returns 503 with detail SCIM provisioning is disabled for this tenant.
Create a separate key for SCIM so it can be rotated independently from keys used by other integrations.
ServiceProviderConfig
GET /ServiceProviderConfig advertises which optional SCIM features Agility supports. Mirror these flags in your client:
| Feature | Supported | Notes |
|---|---|---|
patch | Yes | PATCH is the preferred way to do partial updates |
bulk | No | Send individual requests instead |
filter | Yes | maxResults = 200; only on /Users and /Groups |
changePassword | No | Passwords are managed by the underlying identity provider |
sort | No | Do not send sortBy or sortOrder |
etag | No | If-Match / If-None-Match headers are ignored |
groups | Yes | Groups are virtual — see Groups |
Authentication scheme is oauthbearertoken (the SCIM term for "Bearer in Authorization header").
Schemas
Agility implements the SCIM 2.0 core User and Group schemas, with a small set of opinionated constraints. Fetch the live schemas at runtime when in doubt:
GET /Schemas— list of all schemasGET /Schemas/urn:ietf:params:scim:schemas:core:2.0:UserGET /Schemas/urn:ietf:params:scim:schemas:core:2.0:Group
User schema highlights
| Attribute | Mutability | Notes |
|---|---|---|
id | readOnly | Server-assigned UUID. Use it for all subsequent operations. |
externalId | readWrite | Client-assigned correlation ID. Stored verbatim. |
userName | immutable | Must be the user's work email. Cannot be changed after creation. |
emails | immutable | Primary work email; same value as userName. |
name.givenName | readWrite | Required. |
name.familyName | readWrite | Required. |
phoneNumbers | readWrite | value must be E.164 (e.g. +14155550100). Invalid values are silently dropped. |
roles | readWrite | See Roles. Only honoured when role mapping source is idp_attribute. The attribute name carrying the values is tenant-configurable — roles is just the default. |
active | readWrite | false deactivates (soft-delete); true reactivates a previously deactivated user. |
userName is the only attribute suitable for unique-identifier matching in your client. Matching on id or email is unsafe — id doesn't exist until after creation, and using email directly can produce duplicates if attribute mapping changes.
Roles
Roles are carried by a multi-valued attribute. Allowed values:
adminusersalessales_with_creditapiaccounting
Unknown role values are filtered out silently. Roles are only applied to the user when the tenant's role mapping is configured as From IdP attribute; in all other modes the role attribute on incoming payloads is ignored (the request still succeeds).
Configurable attribute name
The SCIM attribute the server reads role values from is tenant-configurable. It defaults to the standard roles attribute, but a tenant can configure it to any name (typically the local part of an enterprise-extension attribute such as agcRoles) so that an IdP can push roles through whatever mapping they already use.
There's no SCIM-side discovery for the configured name — confirm it with the tenant admin before integrating. If you can't, sending the values under roles is the safest default and will work for any tenant that hasn't overridden it.
The configured name applies to both POST/PUT bodies and PATCH paths. For PATCH, the canonical roles path is always accepted in addition to the configured name, so a client that always uses roles will still work when the tenant hasn't overridden the default.
Agility accepts the role attribute in either of two shapes, matching what real-world IdPs send:
"roles": ["admin", "sales"]
"roles": [{ "value": "admin" }, { "value": "sales" }]
Users
List users — GET /Users
Supports filtering. Pagination parameters (startIndex, count) are accepted but the endpoint returns the full filtered set in one response (capped at filter.maxResults = 200).
Filterable attributes: userName, emails.value, name.givenName, name.familyName, externalId. Supported operators: eq, co (contains), sw (starts with). Unsupported filter attributes are logged as a warning but the request still succeeds (returning all users).
curl "https://api.agilitycredit.net/scim/v2/<tenant-id>/Users?filter=userName%20eq%20%22jane%40acme.com%22" \
-H "Authorization: Bearer <api-key>"
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
"totalResults": 1,
"startIndex": 1,
"itemsPerPage": 1,
"Resources": [
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"id": "6f0e9c6a-1b5a-4f0a-8e3b-1a2c3d4e5f60",
"userName": "jane@acme.com",
"name": { "givenName": "Jane", "familyName": "Doe", "formatted": "Jane Doe" },
"emails": [{ "value": "jane@acme.com", "type": "work", "primary": true }],
"roles": ["admin"],
"active": true,
"meta": { "resourceType": "User", "location": "/scim/v2/<tenant-id>/Users/6f0e9c6a-..." }
}
]
}
Get user — GET /Users/{id}
Returns the user resource. 404 if the user doesn't exist or is deactivated.
Create user — POST /Users
curl -X POST "https://api.agilitycredit.net/scim/v2/<tenant-id>/Users" \
-H "Authorization: Bearer <api-key>" \
-H "Content-Type: application/scim+json" \
-d '{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"userName": "jane@acme.com",
"name": { "givenName": "Jane", "familyName": "Doe" },
"emails": [{ "value": "jane@acme.com", "type": "work", "primary": true }],
"phoneNumbers": [{ "value": "+14155550100", "type": "work" }],
"roles": ["admin"],
"active": true,
"externalId": "acme-hr-1234"
}'
Status codes:
201 Created— new user provisioned.200 OK— user already existed (in Agility or as a deactivated record) and was adopted or reactivated by SCIM. Treat both as success; idempotent retries are safe.400 invalidValue— payload missinguserName/email or schema validation failed.
The response body is the canonical SCIM user resource. Persist id from the response — it's the only stable handle for subsequent operations.
Replace user — PUT /Users/{id}
PUT is a full-resource replace. Send the complete user representation. userName and emails are immutable: if you send a different value, the change is silently ignored (logged on the server side).
Setting active: false in a PUT deactivates the user — equivalent to DELETE.
Update user — PATCH /Users/{id}
PATCH is the recommended way to do partial updates. Both pathed and Okta-style no-path operations are accepted:
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{ "op": "replace", "path": "name.givenName", "value": "Janet" },
{ "op": "replace", "path": "active", "value": false }
]
}
Equivalent Okta-style form (the server normalises this into pathed ops):
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{
"op": "replace",
"value": { "name.givenName": "Janet", "active": false }
}
]
}
Supported paths:
| Path | Notes |
|---|---|
active | false deactivates, true reactivates |
name.givenName, name.familyName | Empty string is rejected (400 invalidValue) |
phoneNumbers, phoneNumbers[type eq "work"].value | Must be E.164; invalid values are dropped |
userName, emails[type eq "work"].value | Ignored (immutable). Request still returns 200. |
roles, roles[primary eq "true"].value, or the tenant-configured role attribute name | Only applied when role mapping source is idp_attribute. Replace-style ops with a value object support Entra's SingleAppRoleAssignment shape. |
Unsupported op values return 400 invalidValue. Unknown paths are logged and ignored — the request still returns 200, which keeps strict IdP clients happy.
Delete user — DELETE /Users/{id}
Soft-deletes (deactivates) the user. The record is retained so that a subsequent POST /Users with the same userName reactivates it rather than creating a duplicate. Returns 204 No Content.
Groups
Agility's groups are virtual — they're derived from the tenant's role mapping configuration, not stored independently. Each configured (idp_group → role) mapping shows up as one SCIM group whose id is the role name and displayName is the IdP group name. As a consequence:
- You cannot create or delete groups via SCIM.
POST /GroupsandDELETE /Groups/{id}succeed for known groups (idempotent) but never mutate the underlying mapping configuration. - Group membership reflects which SCIM-provisioned users hold the role. Portal-managed users are never included and are never modified by group operations.
- Group operations are no-ops unless the tenant's role mapping source is From IdP groups. The endpoints still return
200/204so strict clients don't break.
List groups — GET /Groups
Optional filter parameter — only displayName eq "<name>" is honoured.
Get group — GET /Groups/{role}
The path parameter is the role (admin, user, etc.), which is the group's id. 404 if the role isn't mapped.
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
"id": "admin",
"displayName": "Acme Admins",
"members": [{ "value": "6f0e9c6a-1b5a-...", "display": "jane@acme.com" }],
"meta": { "resourceType": "Group", "location": "/scim/v2/<tenant-id>/Groups/admin" }
}
Create group — POST /Groups
A displayName matching a configured IdP group returns the existing virtual group with 201. An unknown name returns 404. No new mapping is created.
Replace group — PUT /Groups/{role}
The members array is treated as the new full membership set: users not listed lose the role, users newly listed gain it. Only SCIM-provisioned users are touched.
Update group — PATCH /Groups/{role}
Supports add, remove, and replace operations targeting members. Okta-style targeted remove ("path": "members[value eq \"<user-id>\"]") works as well as the unpathed remove of the entire members collection.
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{ "op": "add", "path": "members", "value": [{ "value": "6f0e9c6a-..." }] },
{ "op": "remove", "path": "members[value eq \"a1b2c3d4-...\"]" }
]
}
displayName changes are ignored (logged). Group id/displayName are server-managed.
Delete group — DELETE /Groups/{role}
Removes the role from every SCIM-provisioned user that holds it, but does not remove the mapping itself. Returns 204.
Errors
Error responses use the standard SCIM 2.0 error envelope:
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"],
"status": "400",
"detail": "userName or emails[0].value is required",
"scimType": "invalidValue"
}
| Status | When |
|---|---|
| 400 | Payload validation failed. scimType: invalidValue for schema/value problems. |
| 401 | API key missing or invalid. No SCIM error body — plain HTTP 401. |
| 404 | User, group, or schema not found. |
| 500 | Unexpected backend error. Retry-safe for idempotent verbs (GET, PUT, DELETE). |
| 503 | SCIM is disabled on the tenant. |
The endpoint deliberately tolerates a number of "harmless" client mistakes (unknown PATCH paths, attempts to change userName, unsupported filter attributes) so that strict IdP clients don't fail loudly. These are logged on the server and surface in the Activity tab — see below.
Testing your integration
- Enable SCIM on a test tenant and toggle Log successful events (debug) on. With debug on, every successful
GETis recorded in addition to mutations. - Implement against
GET /ServiceProviderConfigfirst — it confirms auth, base URL, and tenant ID in one call. - Drive a representative end-to-end flow: create a user, PATCH a field, deactivate, reactivate (via a fresh POST), and finally delete.
- Watch the Activity tab on the Identity page. Each request appears with method, path, status code, and the full request/response body — useful for diagnosing attribute-mapping issues.
- Turn debug off before going live to keep the log readable.
See Troubleshooting for common SCIM errors and how to interpret the activity log.