# Agility Credit API documentation (full) Generated from https://apidocs.agilitycredit.com. Index: https://apidocs.agilitycredit.com/llms.txt --- # Agility Credit API: developer home REST API (v3) for credit reports, fraud and identity checks (Experian Precise ID, Prove prefill and identity verification, IDV+, DL Verify), compliant applications (FastApp and compliance links, applicant documents), OFAC and MLA screening, income and employment (Experian Verify, Alt-Fi), Plate to VIN and tenant administration. JSON requests, Bearer API-key authentication. ## Quickstart 1. **Get an API key.** Agility Credit sets up your account and gives you your tenant ID and your first user ID. In the Agility portal go to Settings → Account → API Keys and create a key. UAT and production use separate API keys. No account yet? Email setup@agilitycredit.com. 2. **Make a credit inquiry** (UAT, prequal pull, placeholder applicant; for UAT use the test identities Agility Credit provides with your credentials): ```bash curl -X POST https://api.agc-dev.com/v3/credit-reports \ -H "Authorization: Bearer $AGC_API_KEY" \ -H "Agc-Tenant-Id: $AGC_TENANT_ID" \ -H "Agc-User-Id: $AGC_USER_ID" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "bureau_id": "experian", "pull_type": "prequal", "report_format": "json", "applicant_main": { "first_name": "JANE", "last_name": "SAMPLE", "address": "123 MAIN ST", "city": "ANYTOWN", "state": "TX", "zip": "75001", "income": { "gross_monthly": 5000 } } }' ``` 3. **Read the response.** A 200 returns the report; `body_type` is one of `inquiry`, `inquiry_joint`, `inquiry_multiple_formats`, `inquiry_with_bureau_error`, `compliance`. Keep the `Agc-Applicant-Id` response header and pass it on later calls (OFAC, MLA, documents) to attach them to the same applicant. Fetch the report again with `GET /credit-reports/{id}`. ## Authentication | Header | When | Value | | --- | --- | --- | | `Authorization` | Every request | `Bearer ` | | `Agc-Tenant-Id` | Most endpoints | The tenant ID provided by Agility Credit | | `Agc-User-Id` | Most endpoints | The ID of the user making the request | | `Idempotency-Key` | Every POST | A unique key per request (a GUID) | | `Agc-Applicant-Id` | Optional on most calls; required to read compliance records and documents | Attaches the call to an existing applicant; omit it and a new applicant is created | Idempotency keys are kept for 5 minutes: resending a request with the same `Idempotency-Key` within that time returns the cached response from the first request. ## Environments | Environment | API base URL | Portal | | --- | --- | --- | | Production | `https://api.agilitycredit.net/v3` | `app.agilitycredit.net` | | UAT (test) | `https://api.agc-dev.com/v3` | `app.agc-dev.com` | UAT is a private test environment: Agility Credit provides your UAT credentials. UAT calls are not billed and don't reach live credit bureaus. ## Errors | Status | Meaning | | --- | --- | | 400 | Bad Request: validation failed; the `error` field says which field and why | | 401 | Unauthorized: API key missing, wrong or inactive | | 403 | Forbidden: e.g. the product is not enabled for your tenant | | 404 | Not found (tenants, DL Verify sessions) | | 409 | Conflict: same Idempotency-Key still in progress, a step called out of order, or a DL Verify selfie still processing | | 410 | Gone: prefill session expired; restart with POST /prefill/auth | | 429 | Too Many Requests: rate limit for the current 5-minute window used up | | 500 | Server error | | 502 | DL Verify verification error; retry with the same session_id | ## Rate limits You set your rate limit in your tenant profile, under the API key settings. It is counted over a 5-minute window. `X-Rate-Limit` is the current limit and `X-Rate-Limit-Remaining` the calls left. Going over returns 429. ## Versioning The major version is in the URL (`/v3`). New major versions only for breaking changes; minor and patch versions are reported in the `Agc-Api-Version` response header. ## AI-ready docs Connect Claude, Cursor or VS Code to these docs with the read-only MCP server (no API key needed): ```bash claude mcp add agility-credit-docs -- npx -y @agilitycredit/docs-mcp ``` Or give any assistant https://apidocs.agilitycredit.com/llms.txt, https://apidocs.agilitycredit.com/llms-full.txt, or any page URL + `.md`. Details: https://apidocs.agilitycredit.com/developer-tools.md ## Webhooks Subscribe in the portal under Settings → Account → Webhooks. Each delivery has an `event_id` that stays the same across retries. Add a secret to have deliveries signed with HMAC in the `X-Agc-Signature` header. Guides: https://apidocs.agilitycredit.com/webhooks/create-webhooks.md Source: https://apidocs.agilitycredit.com/ --- # Agility Credit API reference The Agility Credit API Reference section provides comprehensive documentation on the various functionalities and operations available through the Agility Credit API. Users can leverage this section to access detailed information on managing credit data, processing transactions, and integrating credit-related services into their applications with ease and efficiency. Version: 3.0.3 ## Servers - Production: `https://api.agilitycredit.net/v3` - UAT (test): `https://api.agc-dev.com/v3` ## Authentication HTTP Bearer: send `Authorization: Bearer ` on every request. ## Endpoints - [Make an Inquiry](https://apidocs.agilitycredit.com/api/make-inquiry.md): `POST /credit-reports` - [Get a Report](https://apidocs.agilitycredit.com/api/get-credit-report.md): `GET /credit-reports/{id}` - [List Tenants](https://apidocs.agilitycredit.com/api/list-tenants.md): `GET /tenants` - [Create a Tenant](https://apidocs.agilitycredit.com/api/create-tenant.md): `POST /tenants` - [Get a Tenant](https://apidocs.agilitycredit.com/api/get-tenant.md): `GET /tenants/{id}` - [Update a Tenant](https://apidocs.agilitycredit.com/api/update-tenant.md): `PATCH /tenants/{id}` - [Account Opening + Synthetic ID](https://apidocs.agilitycredit.com/api/precise-id-account-opening.md): `POST /precise-id/account-opening` - [OTP Initial](https://apidocs.agilitycredit.com/api/precise-id-otp-initial.md): `POST /precise-id/otp` - [OTP Final](https://apidocs.agilitycredit.com/api/precise-id-otp-final.md): `POST /precise-id/otp/{session_id}` - [Out-of-Wallet Initial](https://apidocs.agilitycredit.com/api/precise-id-oow-initial.md): `POST /precise-id/oow` - [Out-of-Wallet Final](https://apidocs.agilitycredit.com/api/precise-id-oow-final.md): `POST /precise-id/oow/{session_id}` - [GLB](https://apidocs.agilitycredit.com/api/precise-id-glb.md): `POST /precise-id/glb` - [Auth Initial](https://apidocs.agilitycredit.com/api/prefill-auth-initial.md): `POST /prefill/auth` - [Auth Final](https://apidocs.agilitycredit.com/api/prefill-auth-final.md): `POST /prefill/auth/{session_id}` - [Trust + Identity](https://apidocs.agilitycredit.com/api/prefill-trust-and-identity.md): `POST /prefill/trust-and-identity` - [Identity Verify](https://apidocs.agilitycredit.com/api/identity-verify.md): `POST /prefill/verify` - [Identity Verify Plus](https://apidocs.agilitycredit.com/api/idv-plus.md): `POST /verify-id` - [Plate To Vin](https://apidocs.agilitycredit.com/api/run-p-2-v.md): `POST /p2v` - [DL Verify](https://apidocs.agilitycredit.com/api/run-dlv.md): `POST /dlv` - [DL Verify Link](https://apidocs.agilitycredit.com/api/create-dlv-link.md): `POST /links/dlv` - [FastApp Link](https://apidocs.agilitycredit.com/api/fast-app-link.md): `POST /links/fastapp` - [Compliance Link](https://apidocs.agilitycredit.com/api/compliance-link.md): `POST /links/compliance` - [Get Applicant Document](https://apidocs.agilitycredit.com/api/get-applicant-document.md): `GET /applicant-documents/{file_id}` - [List Applicant Documents](https://apidocs.agilitycredit.com/api/list-applicant-documents.md): `GET /applicant-documents` - [DL Verify Results](https://apidocs.agilitycredit.com/api/get-dlv-docs.md): `GET /dlv/{id}` - [OFAC Check](https://apidocs.agilitycredit.com/api/run-ofac.md): `POST /ofac` - [MLA Check](https://apidocs.agilitycredit.com/api/run-mla.md): `POST /mla` - [Alt-Fi Inquiry](https://apidocs.agilitycredit.com/api/run-alt-fi.md): `POST /alt-fi` - [Alt-Fi Income](https://apidocs.agilitycredit.com/api/run-alt-fi-income.md): `POST /alt-fi/income` - [Verify Plus](https://apidocs.agilitycredit.com/api/verify-plus.md): `POST /verify/plus` - [Verify Premium](https://apidocs.agilitycredit.com/api/verify-premium.md): `POST /verify/premium` - [Get Applicant](https://apidocs.agilitycredit.com/api/get-applicant.md): `GET /applicant` - [Get OTP Record](https://apidocs.agilitycredit.com/api/get-otp-record.md): `GET /otp/{id}` - [Get OOW Record](https://apidocs.agilitycredit.com/api/get-oow-record.md): `GET /oow/{id}` - [Get Compliance Records](https://apidocs.agilitycredit.com/api/get-compliance-all.md): `GET /compliance` - [Get Compliance Record by Type](https://apidocs.agilitycredit.com/api/get-compliance-single.md): `GET /compliance/{applicant_type}` OpenAPI spec: https://apidocs.agilitycredit.com/openapi.yaml --- # Developer & AI tools The Agility Credit docs are built for AI-assisted development. Connect Claude, Cursor or VS Code with the [MCP server](#mcp-server), or give any assistant the [Markdown docs and llms.txt](#docs-for-ai-assistants). It then reads the same endpoints, schemas and error codes you do, so the code it writes matches the real API. You'll also find the [OpenAPI spec](#openapi-spec) and a ready-to-use [Postman collection](#postman-collection). ## OpenAPI spec | File | Use it for | | --- | --- | | [`openapi.yaml`](pathname:///openapi.yaml) | The full API: generate clients, import into API tools, validate requests. | | [`agc-scim-openapi.yaml`](pathname:///agc-scim-openapi.yaml) | The SCIM provisioning API. | You can also download the spec from **Export** on the [API reference overview](https://apidocs.agilitycredit.com/api). ## Postman collection [Download the Postman collection](pathname:///agility-credit-api.postman_collection.json), then in Postman choose **Import** and drop in the file. 1. Open the collection's **Variables** tab. 2. Set `apiKey`, `tenantId` and `userId`. Agility Credit provides your tenant ID and first user ID; create API keys in the portal under **Settings → Account → API Keys**. 3. `baseUrl` is set to production. For UAT, change it to `https://api.agc-dev.com/v3` and use your UAT key (UAT and production use separate keys). Every request reuses those variables, and each `POST` sends a fresh `Idempotency-Key`. Request bodies contain placeholder values only. ## Docs for AI assistants The docs are available as plain Markdown, so AI assistants and tools that don't run JavaScript get the full reference, including parameters and schemas. | URL | What it is | | --- | --- | | [`/llms.txt`](pathname:///llms.txt) | An index of every page, following the [llms.txt](https://llmstxt.org) convention. | | [`/llms-full.txt`](pathname:///llms-full.txt) | The whole documentation in one file. Paste it into a chat or a project's knowledge. | | Any page URL + `.md` | The Markdown version of that page, for example [`/api/make-inquiry.md`](pathname:///api/make-inquiry.md). | ## MCP server The Agility Credit docs MCP server lets assistants that support the [Model Context Protocol](https://modelcontextprotocol.io), such as Claude, Cursor and VS Code, look up endpoints, schemas, headers and errors while you work. It is **read-only**: it reads the published OpenAPI spec and never calls the Agility Credit API. It needs no API key and sends no data anywhere except to fetch the spec from this site. It requires [Node.js](https://nodejs.org) 18 or later. **Claude Code** ```bash claude mcp add agility-credit-docs -- npx -y @agilitycredit/docs-mcp ``` **Claude Desktop, Cursor** (`claude_desktop_config.json` or `.cursor/mcp.json`): ```json { "mcpServers": { "agility-credit-docs": { "command": "npx", "args": ["-y", "@agilitycredit/docs-mcp"] } } } ``` **VS Code** (`.vscode/mcp.json`): ```json { "servers": { "agility-credit-docs": { "type": "stdio", "command": "npx", "args": ["-y", "@agilitycredit/docs-mcp"] } } } ``` The package is [`@agilitycredit/docs-mcp`](https://www.npmjs.com/package/@agilitycredit/docs-mcp) on npm. | Tool | What it does | | --- | --- | | `list_endpoints` | Lists endpoints, optionally filtered by tag or keyword. | | `get_endpoint` | Full details for one endpoint: headers, request body, responses. | | `get_schema` | A named schema with references resolved. | | `search_api` | Keyword search across endpoints and schemas. | | `get_platform_basics` | Environments, authentication, common headers, rate limits and error codes. | Then ask your assistant things like *"Which headers does `POST /credit-reports` need?"* or *"Write a Python function that runs an OFAC check."* Source: https://apidocs.agilitycredit.com/developer-tools --- # Make an Inquiry `POST https://api.agilitycredit.net/v3/credit-reports` Docs page: https://apidocs.agilitycredit.com/api/make-inquiry · Tag: Credit Reports The "Make an Inquiry" section allows users to submit a request for credit information based on provided applicant details. By filling in required applicant information, users can retrieve borrower and co-borrower credit profiles, employment history, credit scores, public records, trade lines, collections data, inquiries, and summary reports. This section enables users to access comprehensive credit information for decision-making processes. Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `Idempotency-Key` | header | yes | A unique, idempotent key. We recommend to use a guid | | `Agc-Tenant-Id` | header | yes | The tenant ID provided by Agility Credit. | | `Agc-User-Id` | header | yes | The requestor user ID. | | `Agc-Applicant-Id` | header | | If passed, will assign the report to that applicant. If not, will create a new applicant ID | ## Request body (`application/json`), required Inquiry request payload. > ⚠️ **Data is automatically sanitized** before it is sent to the bureau — > disallowed characters are stripped, values are reformatted or truncated to > the bureau's limits, and values that cannot be made valid are dropped. Your > original values are still stored on the applicant record. See > [**Credit Report Data Sanitization**](https://apidocs.agilitycredit.com/data-sanitization/credit-report) for the > full per-field rules for each bureau. > > **decisioning** is an optional service that requires a custom configuration for > a successful request. Schema: `Inquiry` | Field | Type | Required | Description | | --- | --- | --- | --- | | `bureau_id` | string: `equifax`, `transunion`, `experian` | yes | | | `pull_type` | string: `prequal`, `hard` | yes | | | `applicant_main` | object | yes | | | `applicant_main.first_name` | string | yes | The first name of the applicant. [REQUIRED] | | `applicant_main.middle_name` | string | | The middle name of the applicant. Middle name can be an initial. | | `applicant_main.last_name` | string | yes | The last name of the applicant. [REQUIRED] | | `applicant_main.ssn` | string | | The SSN of the applicant. Required if pull_type is set to 'hard'. | | `applicant_main.dob` | string | | The date of birth of the applicant. Required if pull_type is set to 'hard'. | | `applicant_main.phone` | string | | The phone number of the applicant. | | `applicant_main.email` | string | | The email address of the applicant. | | `applicant_main.address` | string | yes | The address of the applicant. The format is '{house_number} {street_name} {street_type} {direction}'. street_type and direction are optional. [REQUIRED] | | `applicant_main.zip` | string | yes | The ZIP code of the applicant. [REQUIRED] | | `applicant_main.state` | string | yes | The state of the applicant. [REQUIRED] | | `applicant_main.city` | string | yes | The city of the applicant. Abbreviation of the city name. E.g 'NY'. [REQUIRED] | | `applicant_main.income` | object | | | | `applicant_main.income.gross_monthly` | integer | yes | Gross monthly income, greater than zero. [REQUIRED] | | `applicant_main.income.other_monthly` | integer | | Other monthly income, greater than zero. | | `applicant_main.income.other_source` | string: `aid_for_dependent_children`, `child_support`, `cola`, `disability`, `fssa`, `housing_allowances`, `military_basic_allowance_for_subsistence`, `municipal_bond_interest`, `non_taxable_sources_of_income`, `other`, … (5 more) | | Source of other income. | | `applicant_co` | object | | | | `applicant_co.first_name` | string | yes | The first name of the applicant. [REQUIRED] | | `applicant_co.middle_name` | string | | The middle name of the applicant. Middle name can be an initial. | | `applicant_co.last_name` | string | yes | The last name of the applicant. [REQUIRED] | | `applicant_co.ssn` | string | | The SSN of the applicant. Required if pull_type is set to 'hard'. | | `applicant_co.dob` | string | | The date of birth of the applicant. Required if pull_type is set to 'hard'. | | `applicant_co.phone` | string | | The phone number of the applicant. | | `applicant_co.email` | string | | The email address of the applicant. | | `applicant_co.address` | string | yes | The address of the applicant. The format is '{house_number} {street_name} {street_type} {direction}'. street_type and direction are optional. [REQUIRED] | | `applicant_co.zip` | string | yes | The ZIP code of the applicant. [REQUIRED] | | `applicant_co.state` | string | yes | The state of the applicant. [REQUIRED] | | `applicant_co.city` | string | yes | The city of the applicant. Abbreviation of the city name. E.g 'NY'. [REQUIRED] | | `applicant_co.income` | object | | | | `applicant_co.income.gross_monthly` | integer | yes | Gross monthly income, greater than zero. [REQUIRED] | | `applicant_co.income.other_monthly` | integer | | Other monthly income, greater than zero. | | `applicant_co.income.other_source` | string: `aid_for_dependent_children`, `child_support`, `cola`, `disability`, `fssa`, `housing_allowances`, `military_basic_allowance_for_subsistence`, `municipal_bond_interest`, `non_taxable_sources_of_income`, `other`, … (5 more) | | Source of other income. | | `report_format` | one of: string: `json`, `html`, `xml`, `raw`, `pdf`, `link`, array of string: `json`, `html`, `xml`, `raw`, `pdf` | yes | | | `features` | array of string: `mla`, `ofac`, `auto_summary`, `shield`, `decisioning` | | | | `decisioning_attributes` | array of object | | Attributes passed to the bureau's decisioning engine. Required when `features` contains `decisioning`. | | `decisioning_attributes[].name` | string: `LOANTYPE`, `PORTFOLIOTYPE`, `MNTHLYHOUSEPYMNT`, `OCCUPANCY`, `MOSATRES`, `CRDTCARDTYP`, `CRDTCARDESTPYMNT`, `CRDTCARDPURPTYP`, `CRDTCARDREQLMT`, `EMPOFFININST`, … (45 more) | yes | The attribute name. It determines the allowed type and limit of `value`: \| Name \| Type \| Limit \| \| --- \| --- \| --- \| \| `LOANTYPE` \| string \| max length 30 \| \| `PORTFOLIOTYPE` \| string \| max length 30 \| \| `MNTHLYHOUSEPYMNT` \| integer \| ±999… | | `decisioning_attributes[].value` | string \| integer | yes | | | `score_models` | one of: equifax, experian, transunion | | A list of score models to request. All values must belong to a single bureau column (equifax, experian, or transunion); models from different bureaus cannot be combined in one request. | Example: ```json { "bureau_id": "experian", "pull_type": "prequal", "report_format": "json", "score_models": [], "features": [], "applicant_main": { "first_name": "JANE", "last_name": "SAMPLE", "phone": "", "email": "", "address": "123 MAIN ST", "city": "ANYTOWN", "state": "TX", "zip": "750010000", "income": { "gross_monthly": 5000, "other_monthly": 2000, "other_source": "second_employer" } } } ``` ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | Inquiry processed successfully | one of: InquiryResponse, InquiryResponseWithBureauError, InquiryResponseJoint, InquiryResponseMultipleFormats, ComplianceResponse | | 400 | Bad Request | BadRequestResponse | | 401 | Unauthorized | UnauthorizedResponse | | 429 | Too Many Requests | TooManyRequestsResponse | | 500 | Internal Server Error | AgilityServerErrorResponse | The 200 body is one of: `InquiryResponse`, `InquiryResponseWithBureauError`, `InquiryResponseJoint`, `InquiryResponseMultipleFormats`, `ComplianceResponse` (discriminator: `body_type`). ### Error bodies 400: ```json { "status": "Bad Request - Invalid Parameters", "error": "Failed schema validation. Error: data.pull_type must be one of ['prequal', 'hard'], Path: ['data', 'pull_type'], Data: prequalification" } ``` 401: ```json { "message": "Unauthorized" } ``` 429: ```json { "error": "Too Many Requests" } ``` 500: ```json { "message": "Agility Server Error" } ``` --- # Get a Report `GET https://api.agilitycredit.net/v3/credit-reports/{id}` Docs page: https://apidocs.agilitycredit.com/api/get-credit-report · Tag: Credit Reports Get a credit report with report ID Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | UUID of the report to be fetched. | | `Agc-Tenant-Id` | header | yes | The tenant ID provided by Agility Credit. | | `Agc-User-Id` | header | yes | The requestor user ID. | | `Agc-Applicant-Id` | header | | If passed, will assign the report to that applicant. If not, will create a new applicant ID | | `format` | query | yes | Format of the report to be returned. | ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | Inquiry processed successfully | one of: InquiryResponse, InquiryResponseWithBureauError, InquiryResponseJoint, InquiryResponseMultipleFormats | | 400 | Bad Request | BadRequestResponse | | 401 | Unauthorized | UnauthorizedResponse | | 429 | Too Many Requests | TooManyRequestsResponse | | 500 | Internal Server Error | AgilityServerErrorResponse | The 200 body is one of: `InquiryResponse`, `InquiryResponseWithBureauError`, `InquiryResponseJoint`, `InquiryResponseMultipleFormats` (discriminator: `body_type`). ### Error bodies 400: ```json { "status": "Bad Request - Invalid Parameters", "error": "Failed schema validation. Error: data.pull_type must be one of ['prequal', 'hard'], Path: ['data', 'pull_type'], Data: prequalification" } ``` 401: ```json { "message": "Unauthorized" } ``` 429: ```json { "error": "Too Many Requests" } ``` 500: ```json { "message": "Agility Server Error" } ``` --- # Account Opening + Synthetic ID `POST https://api.agilitycredit.net/v3/precise-id/account-opening` Docs page: https://apidocs.agilitycredit.com/api/precise-id-account-opening · Tag: Compliance The "Account Opening" endpoint provides comprehensive identity verification through advanced data analytics and cross-reference validation. This service performs real-time identity verification by analyzing and correlating multiple identity elements (name, address, phone, SSN, date of birth) against extensive databases and public records. It includes fraud detection capabilities, OFAC sanctions screening, synthetic identity detection, and provides detailed cross-reference indicators showing how different identity elements verify against each other. The service returns risk scores, verification levels, and comprehensive identity assessment results to support high-confidence decision-making in identity verification processes. Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `Idempotency-Key` | header | yes | A unique, idempotent key. We recommend to use a guid | | `Agc-Tenant-Id` | header | yes | The tenant ID provided by Agility Credit. | | `Agc-User-Id` | header | yes | The requestor user ID. | | `Agc-Applicant-Id` | header | | If passed, will assign the report to that applicant. If not, will create a new applicant ID | ## Request body (`application/json`), required Inquiry request payload. > ⚠️ **Data is automatically sanitized** before it is sent to the bureau — > disallowed characters are stripped, values are reformatted or truncated to > the bureau's limits, and values that cannot be made valid are dropped. Your > original values are still stored on the applicant record. See > [**Precise ID Data Sanitization**](https://apidocs.agilitycredit.com/data-sanitization/precise-id) for the > full per-field rules. Schema: `PreciseID` | Field | Type | Required | Description | | --- | --- | --- | --- | | `format` | string: `json`, `raw`, `xml` | | | | `applicant` | object | yes | | | `applicant.first_name` | string | yes | Applicant first name. Minimum entry is the first initial. Compound names should be separated with a hyphen or space. Allowable special characters: hyphen, space, apostrophe. | | `applicant.middle_name` | string | | Applicant middle name; a middle initial is acceptable. Blanks and special characters are not allowed. Omit if not available. | | `applicant.last_name` | string | yes | Applicant surname; must be two or more characters. Split double surnames with a hyphen (e.g. Smith-Jones). Apostrophe usage is limited to the letters D, L and O. Allowable special characters: hyphen, space, apostrophe. The full name (last … | | `applicant.suffix` | string: `JR`, `SR`, `II`, `III`, `IV`, `V`, `VI`, `VII`, `VIII`, `IX` | | Generation / name suffix code. | | `applicant.ssn` | string | yes | Social Security Number (required); 4, 9 or 11 characters. Allowable special characters: hyphen, space. | | `applicant.dob` | string | | Applicant date of birth in YYYY-MM-DD format (a bare YYYY is also accepted and treated as dob_year). Use dob when the full birth date is available; otherwise use dob_year. | | `applicant.dob_year` | string | | Applicant year of birth in YYYY format. Use when the full date of birth is not available. | | `applicant.age` | integer | | Applicant age. Domain 14 - 120. | | `applicant.phone_mobile` | string | | Applicant mobile telephone number; 10 digits (e.g. 5551234567). Formatting characters and a leading country-code 1 are removed automatically; the value must reduce to exactly 10 digits or it is dropped from the bureau request. | | `applicant.phone_home` | string | | Applicant home telephone number; 10 digits (e.g. 5551234567). Formatting characters and a leading country-code 1 are removed automatically; the value must reduce to exactly 10 digits or it is dropped from the bureau request. | | `applicant.phone_work` | string | | Applicant work telephone number; 10 digits (e.g. 5551234567). Formatting characters and a leading country-code 1 are removed automatically; the value must reduce to exactly 10 digits or it is dropped from the bureau request. | | `applicant.emails` | array of string | | Up to 3 applicant email addresses. Allowable special characters: at sign, underscore, hyphen, period. | | `applicant.address` | object | yes | Applicant current address. The full address (street + city + state + zip) cannot exceed 118 characters. | | `applicant.address.po_box_number` | string | | PO Box number, included as part of the street line. | | `applicant.address.street1` | string | yes | Street address, PO Box or route number. Allowable special characters: forward slash, period, hyphen, pound sign, space. | | `applicant.address.street2` | string | | Additional street address line. | | `applicant.address.city` | string | yes | City name. Allowable special characters: apostrophe, period, hyphen, space. | | `applicant.address.state_code` | string | yes | Two-character state code. | | `applicant.address.zip` | string | yes | ZIP code. Allowable special characters: hyphen. | | `applicant.address.country_code` | string | | ISO country code. | | `applicant.address.time_at_address` | object | | Time at the current residence. Provide both years and months together — residence-duration reporting is omitted if only one is supplied. | | `applicant.address_previous` | object | | Applicant previous address (informational only). The full address (street + city + state + zip) cannot exceed 118 characters. | | `applicant.address_previous.po_box_number` | string | | PO Box number, included as part of the street line. | | `applicant.address_previous.street1` | string | yes | Street address, PO Box or route number. Allowable special characters: forward slash, period, hyphen, pound sign, space. | | `applicant.address_previous.street2` | string | | Additional street address line. | | `applicant.address_previous.city` | string | yes | City name. Allowable special characters: apostrophe, period, hyphen, space. | | `applicant.address_previous.state_code` | string | yes | Two-character state code. | | `applicant.address_previous.zip` | string | yes | ZIP code. Allowable special characters: hyphen. | | `applicant.address_previous.country_code` | string | | ISO country code. | | `applicant.address_previous.time_at_address` | object | | Time at the previous residence. Provide both years and months together — residence-duration reporting is omitted if only one is supplied. | | `applicant.employment` | object | | Applicant current employment. The full employment data (company + address + city + state + zip) cannot exceed 73 characters. | | `applicant.employment.employer_name` | string | | Company name; required when employment is supplied. Allowable special characters: pound sign, ampersand, hyphen, space. | | `applicant.employment.employer_address` | object | | | | `applicant.employment.income` | object | | | | `applicant.employment_previous` | object | | Applicant previous employment. | | `applicant.employment_previous.employer_name` | string | | Company name. Allowable special characters: pound sign, ampersand, hyphen, space. | | `applicant.employment_previous.employer_address` | object | | | | `applicant.employment_previous.income` | object | | | | `applicant.mothers_maiden_name` | string | | Applicant mother's maiden name. | | `applicant.spouse_name` | string | | Full name of the applicant's spouse. | | `applicant.no_of_dependents` | integer | | Total number of dependents, excluding the applicant. | | `applicant.driver_license` | object | | | | `applicant.driver_license.number` | string | | Driver's license number; required when driver_license is supplied. A minimum of two characters must be entered. Allowable special characters: space, asterisk, hyphen. | | `applicant.driver_license.state` | string | | Two-character driver's license state code; required when driver_license is supplied. | ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | Inquiry processed successfully | one of: PreciseIDResponseOPT6, PreciseIDResponseOPT10 | | 400 | Bad Request | BadRequestResponsePID | | 401 | Unauthorized | UnauthorizedResponse | | 403 | Forbidden | UnauthorizedResponse | | 429 | Too Many Requests | TooManyRequestsResponse | | 500 | Internal Server Error | AgilityServerErrorResponse | The 200 body is one of: `PreciseIDResponseOPT6`, `PreciseIDResponseOPT10`. ### Error bodies 400: ```json { "status": "Bad Request - Validation Failed", "error": "Failed schema validation. Error: data must contain ['format'] properties, Path: ['data'], Data: {'format1': 'xml', 'applicant': {'first_name': 'MATHEW', 'middle_name': 'J', 'last_name': 'Doyle', 'ssn': '666361452', 'address': {'street1': '10046 COLONNADE DR', 'city': 'Tampa', 'state_code': 'FL', 'zip': '336471863'}}, 'stage': 'v3'}" } ``` 401: ```json { "message": "Unauthorized" } ``` 403: ```json { "message": "Unauthorized" } ``` 429: ```json { "error": "Too Many Requests" } ``` 500: ```json { "message": "Agility Server Error" } ``` --- # GLB `POST https://api.agilitycredit.net/v3/precise-id/glb` Docs page: https://apidocs.agilitycredit.com/api/precise-id-glb · Tag: Compliance Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `Idempotency-Key` | header | yes | A unique, idempotent key. We recommend to use a guid | | `Agc-Tenant-Id` | header | yes | The tenant ID provided by Agility Credit. | | `Agc-User-Id` | header | yes | The requestor user ID. | | `Agc-Applicant-Id` | header | | If passed, will assign the report to that applicant. If not, will create a new applicant ID | ## Request body (`application/json`), required Inquiry request payload. > ⚠️ **Data is automatically sanitized** before it is sent to the bureau — > disallowed characters are stripped, values are reformatted or truncated to > the bureau's limits, and values that cannot be made valid are dropped. Your > original values are still stored on the applicant record. See > [**Precise ID data sanitization**](https://apidocs.agilitycredit.com/data-sanitization/precise-id) for the > full per-field rules. Schema: `PreciseID` | Field | Type | Required | Description | | --- | --- | --- | --- | | `format` | string: `json`, `raw`, `xml` | | | | `applicant` | object | yes | | | `applicant.first_name` | string | yes | Applicant first name. Minimum entry is the first initial. Compound names should be separated with a hyphen or space. Allowable special characters: hyphen, space, apostrophe. | | `applicant.middle_name` | string | | Applicant middle name; a middle initial is acceptable. Blanks and special characters are not allowed. Omit if not available. | | `applicant.last_name` | string | yes | Applicant surname; must be two or more characters. Split double surnames with a hyphen (e.g. Smith-Jones). Apostrophe usage is limited to the letters D, L and O. Allowable special characters: hyphen, space, apostrophe. The full name (last … | | `applicant.suffix` | string: `JR`, `SR`, `II`, `III`, `IV`, `V`, `VI`, `VII`, `VIII`, `IX` | | Generation / name suffix code. | | `applicant.ssn` | string | yes | Social Security Number (required); 4, 9 or 11 characters. Allowable special characters: hyphen, space. | | `applicant.dob` | string | | Applicant date of birth in YYYY-MM-DD format (a bare YYYY is also accepted and treated as dob_year). Use dob when the full birth date is available; otherwise use dob_year. | | `applicant.dob_year` | string | | Applicant year of birth in YYYY format. Use when the full date of birth is not available. | | `applicant.age` | integer | | Applicant age. Domain 14 - 120. | | `applicant.phone_mobile` | string | | Applicant mobile telephone number; 10 digits (e.g. 5551234567). Formatting characters and a leading country-code 1 are removed automatically; the value must reduce to exactly 10 digits or it is dropped from the bureau request. | | `applicant.phone_home` | string | | Applicant home telephone number; 10 digits (e.g. 5551234567). Formatting characters and a leading country-code 1 are removed automatically; the value must reduce to exactly 10 digits or it is dropped from the bureau request. | | `applicant.phone_work` | string | | Applicant work telephone number; 10 digits (e.g. 5551234567). Formatting characters and a leading country-code 1 are removed automatically; the value must reduce to exactly 10 digits or it is dropped from the bureau request. | | `applicant.emails` | array of string | | Up to 3 applicant email addresses. Allowable special characters: at sign, underscore, hyphen, period. | | `applicant.address` | object | yes | Applicant current address. The full address (street + city + state + zip) cannot exceed 118 characters. | | `applicant.address.po_box_number` | string | | PO Box number, included as part of the street line. | | `applicant.address.street1` | string | yes | Street address, PO Box or route number. Allowable special characters: forward slash, period, hyphen, pound sign, space. | | `applicant.address.street2` | string | | Additional street address line. | | `applicant.address.city` | string | yes | City name. Allowable special characters: apostrophe, period, hyphen, space. | | `applicant.address.state_code` | string | yes | Two-character state code. | | `applicant.address.zip` | string | yes | ZIP code. Allowable special characters: hyphen. | | `applicant.address.country_code` | string | | ISO country code. | | `applicant.address.time_at_address` | object | | Time at the current residence. Provide both years and months together — residence-duration reporting is omitted if only one is supplied. | | `applicant.address_previous` | object | | Applicant previous address (informational only). The full address (street + city + state + zip) cannot exceed 118 characters. | | `applicant.address_previous.po_box_number` | string | | PO Box number, included as part of the street line. | | `applicant.address_previous.street1` | string | yes | Street address, PO Box or route number. Allowable special characters: forward slash, period, hyphen, pound sign, space. | | `applicant.address_previous.street2` | string | | Additional street address line. | | `applicant.address_previous.city` | string | yes | City name. Allowable special characters: apostrophe, period, hyphen, space. | | `applicant.address_previous.state_code` | string | yes | Two-character state code. | | `applicant.address_previous.zip` | string | yes | ZIP code. Allowable special characters: hyphen. | | `applicant.address_previous.country_code` | string | | ISO country code. | | `applicant.address_previous.time_at_address` | object | | Time at the previous residence. Provide both years and months together — residence-duration reporting is omitted if only one is supplied. | | `applicant.employment` | object | | Applicant current employment. The full employment data (company + address + city + state + zip) cannot exceed 73 characters. | | `applicant.employment.employer_name` | string | | Company name; required when employment is supplied. Allowable special characters: pound sign, ampersand, hyphen, space. | | `applicant.employment.employer_address` | object | | | | `applicant.employment.income` | object | | | | `applicant.employment_previous` | object | | Applicant previous employment. | | `applicant.employment_previous.employer_name` | string | | Company name. Allowable special characters: pound sign, ampersand, hyphen, space. | | `applicant.employment_previous.employer_address` | object | | | | `applicant.employment_previous.income` | object | | | | `applicant.mothers_maiden_name` | string | | Applicant mother's maiden name. | | `applicant.spouse_name` | string | | Full name of the applicant's spouse. | | `applicant.no_of_dependents` | integer | | Total number of dependents, excluding the applicant. | | `applicant.driver_license` | object | | | | `applicant.driver_license.number` | string | | Driver's license number; required when driver_license is supplied. A minimum of two characters must be entered. Allowable special characters: space, asterisk, hyphen. | | `applicant.driver_license.state` | string | | Two-character driver's license state code; required when driver_license is supplied. | ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | Inquiry processed successfully | one of: PreciseIDResponseOPT20 | | 400 | Bad Request | BadRequestResponse | | 401 | Unauthorized | UnauthorizedResponse | | 403 | Forbidden | UnauthorizedResponse | | 429 | Too Many Requests | TooManyRequestsResponse | | 500 | Internal Server Error | AgilityServerErrorResponse | The 200 body is one of: `PreciseIDResponseOPT20`. ### Error bodies 400: ```json { "status": "Bad Request - Invalid Parameters", "error": "Failed schema validation. Error: data.pull_type must be one of ['prequal', 'hard'], Path: ['data', 'pull_type'], Data: prequalification" } ``` 401: ```json { "message": "Unauthorized" } ``` 403: ```json { "message": "Unauthorized" } ``` 429: ```json { "error": "Too Many Requests" } ``` 500: ```json { "message": "Agility Server Error" } ``` --- # OTP Initial `POST https://api.agilitycredit.net/v3/precise-id/otp` Docs page: https://apidocs.agilitycredit.com/api/precise-id-otp-initial · Tag: Precise ID The "OTP Initial" endpoint provides One-Time Passcode (OTP) authentication for identity verification during account opening processes. This service generates and validates SMS-based OTP codes to verify that the applicant has access to the provided phone number. The workflow consists of two steps: first, an OTP is generated and sent to the applicant's phone number, then the applicant submits the received code for validation. This multi-factor authentication approach enhances security by confirming phone number ownership and reducing the risk of identity fraud during account creation. Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `Idempotency-Key` | header | yes | A unique, idempotent key. We recommend to use a guid | | `Agc-Tenant-Id` | header | yes | The tenant ID provided by Agility Credit. | | `Agc-User-Id` | header | yes | The requestor user ID. | | `Agc-Applicant-Id` | header | | If passed, will assign the report to that applicant. If not, will create a new applicant ID | ## Request body (`application/json`), required Inquiry request payload. > ⚠️ **Data is automatically sanitized** before it is sent to the bureau — > disallowed characters are stripped, values are reformatted or truncated to > the bureau's limits, and values that cannot be made valid are dropped. Your > original values are still stored on the applicant record. See > [**Precise ID data sanitization**](https://apidocs.agilitycredit.com/data-sanitization/precise-id) for the > full per-field rules. Variant 1: `PreciseID` | Field | Type | Required | Description | | --- | --- | --- | --- | | `format` | string: `json`, `raw`, `xml` | | | | `applicant` | object | yes | | | `applicant.first_name` | string | yes | Applicant first name. Minimum entry is the first initial. Compound names should be separated with a hyphen or space. Allowable special characters: hyphen, space, apostrophe. | | `applicant.middle_name` | string | | Applicant middle name; a middle initial is acceptable. Blanks and special characters are not allowed. Omit if not available. | | `applicant.last_name` | string | yes | Applicant surname; must be two or more characters. Split double surnames with a hyphen (e.g. Smith-Jones). Apostrophe usage is limited to the letters D, L and O. Allowable special characters: hyphen, space, apostrophe. The full name (last … | | `applicant.suffix` | string: `JR`, `SR`, `II`, `III`, `IV`, `V`, `VI`, `VII`, `VIII`, `IX` | | Generation / name suffix code. | | `applicant.ssn` | string | yes | Social Security Number (required); 4, 9 or 11 characters. Allowable special characters: hyphen, space. | | `applicant.dob` | string | | Applicant date of birth in YYYY-MM-DD format (a bare YYYY is also accepted and treated as dob_year). Use dob when the full birth date is available; otherwise use dob_year. | | `applicant.dob_year` | string | | Applicant year of birth in YYYY format. Use when the full date of birth is not available. | | `applicant.age` | integer | | Applicant age. Domain 14 - 120. | | `applicant.phone_mobile` | string | | Applicant mobile telephone number; 10 digits (e.g. 5551234567). Formatting characters and a leading country-code 1 are removed automatically; the value must reduce to exactly 10 digits or it is dropped from the bureau request. | | `applicant.phone_home` | string | | Applicant home telephone number; 10 digits (e.g. 5551234567). Formatting characters and a leading country-code 1 are removed automatically; the value must reduce to exactly 10 digits or it is dropped from the bureau request. | | `applicant.phone_work` | string | | Applicant work telephone number; 10 digits (e.g. 5551234567). Formatting characters and a leading country-code 1 are removed automatically; the value must reduce to exactly 10 digits or it is dropped from the bureau request. | | `applicant.emails` | array of string | | Up to 3 applicant email addresses. Allowable special characters: at sign, underscore, hyphen, period. | | `applicant.address` | object | yes | Applicant current address. The full address (street + city + state + zip) cannot exceed 118 characters. | | `applicant.address.po_box_number` | string | | PO Box number, included as part of the street line. | | `applicant.address.street1` | string | yes | Street address, PO Box or route number. Allowable special characters: forward slash, period, hyphen, pound sign, space. | | `applicant.address.street2` | string | | Additional street address line. | | `applicant.address.city` | string | yes | City name. Allowable special characters: apostrophe, period, hyphen, space. | | `applicant.address.state_code` | string | yes | Two-character state code. | | `applicant.address.zip` | string | yes | ZIP code. Allowable special characters: hyphen. | | `applicant.address.country_code` | string | | ISO country code. | | `applicant.address.time_at_address` | object | | Time at the current residence. Provide both years and months together — residence-duration reporting is omitted if only one is supplied. | | `applicant.address_previous` | object | | Applicant previous address (informational only). The full address (street + city + state + zip) cannot exceed 118 characters. | | `applicant.address_previous.po_box_number` | string | | PO Box number, included as part of the street line. | | `applicant.address_previous.street1` | string | yes | Street address, PO Box or route number. Allowable special characters: forward slash, period, hyphen, pound sign, space. | | `applicant.address_previous.street2` | string | | Additional street address line. | | `applicant.address_previous.city` | string | yes | City name. Allowable special characters: apostrophe, period, hyphen, space. | | `applicant.address_previous.state_code` | string | yes | Two-character state code. | | `applicant.address_previous.zip` | string | yes | ZIP code. Allowable special characters: hyphen. | | `applicant.address_previous.country_code` | string | | ISO country code. | | `applicant.address_previous.time_at_address` | object | | Time at the previous residence. Provide both years and months together — residence-duration reporting is omitted if only one is supplied. | | `applicant.employment` | object | | Applicant current employment. The full employment data (company + address + city + state + zip) cannot exceed 73 characters. | | `applicant.employment.employer_name` | string | | Company name; required when employment is supplied. Allowable special characters: pound sign, ampersand, hyphen, space. | | `applicant.employment.employer_address` | object | | | | `applicant.employment.income` | object | | | | `applicant.employment_previous` | object | | Applicant previous employment. | | `applicant.employment_previous.employer_name` | string | | Company name. Allowable special characters: pound sign, ampersand, hyphen, space. | | `applicant.employment_previous.employer_address` | object | | | | `applicant.employment_previous.income` | object | | | | `applicant.mothers_maiden_name` | string | | Applicant mother's maiden name. | | `applicant.spouse_name` | string | | Full name of the applicant's spouse. | | `applicant.no_of_dependents` | integer | | Total number of dependents, excluding the applicant. | | `applicant.driver_license` | object | | | | `applicant.driver_license.number` | string | | Driver's license number; required when driver_license is supplied. A minimum of two characters must be entered. Allowable special characters: space, asterisk, hyphen. | | `applicant.driver_license.state` | string | | Two-character driver's license state code; required when driver_license is supplied. | ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | Inquiry processed successfully | one of: PreciseIDResponseOTPInitial | | 400 | Bad Request | BadRequestResponse | | 401 | Unauthorized | UnauthorizedResponse | | 403 | Forbidden | UnauthorizedResponse | | 429 | Too Many Requests | TooManyRequestsResponse | | 500 | Internal Server Error | AgilityServerErrorResponse | The 200 body is one of: `PreciseIDResponseOTPInitial`. ### Error bodies 400: ```json { "status": "Bad Request - Invalid Parameters", "error": "Failed schema validation. Error: data.pull_type must be one of ['prequal', 'hard'], Path: ['data', 'pull_type'], Data: prequalification" } ``` 401: ```json { "message": "Unauthorized" } ``` 403: ```json { "message": "Unauthorized" } ``` 429: ```json { "error": "Too Many Requests" } ``` 500: ```json { "message": "Agility Server Error" } ``` --- # OTP Final `POST https://api.agilitycredit.net/v3/precise-id/otp/{session_id}` Docs page: https://apidocs.agilitycredit.com/api/precise-id-otp-final · Tag: Precise ID The "OTP Initial" endpoint provides One-Time Passcode (OTP) authentication for identity verification during account opening processes. This service generates and validates SMS-based OTP codes to verify that the applicant has access to the provided phone number. The workflow consists of two steps: first, an OTP is generated and sent to the applicant's phone number, then the applicant submits the received code for validation. This multi-factor authentication approach enhances security by confirming phone number ownership and reducing the risk of identity fraud during account creation. Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `session_id` | path | yes | The session identifier returned from the corresponding initial request. | | `Idempotency-Key` | header | yes | A unique, idempotent key. We recommend to use a guid | | `Agc-Tenant-Id` | header | yes | The tenant ID provided by Agility Credit. | | `Agc-User-Id` | header | yes | The requestor user ID. | | `Agc-Applicant-Id` | header | | If passed, will assign the report to that applicant. If not, will create a new applicant ID | ## Request body (`application/json`), required Inquiry request payload Variant 1: `PreciseIDOTPFinal` | Field | Type | Required | Description | | --- | --- | --- | --- | | `otp_code` | string | yes | | ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | Inquiry processed successfully | one of: PreciseIDResponseOTPFinal | | 400 | Bad Request | BadRequestResponse | | 401 | Unauthorized | UnauthorizedResponse | | 403 | Forbidden | UnauthorizedResponse | | 429 | Too Many Requests | TooManyRequestsResponse | | 500 | Internal Server Error | AgilityServerErrorResponse | The 200 body is one of: `PreciseIDResponseOTPFinal`. ### Error bodies 400: ```json { "status": "Bad Request - Invalid Parameters", "error": "Failed schema validation. Error: data.pull_type must be one of ['prequal', 'hard'], Path: ['data', 'pull_type'], Data: prequalification" } ``` 401: ```json { "message": "Unauthorized" } ``` 403: ```json { "message": "Unauthorized" } ``` 429: ```json { "error": "Too Many Requests" } ``` 500: ```json { "message": "Agility Server Error" } ``` --- # Out-of-Wallet Initial `POST https://api.agilitycredit.net/v3/precise-id/oow` Docs page: https://apidocs.agilitycredit.com/api/precise-id-oow-initial · Tag: Precise ID The "Out of Wallet Questions - Initial Request" endpoint performs identity verification through knowledge-based questions derived from the applicant's credit history and public records. This service presents multiple-choice questions about the applicant's financial history, previous addresses, loans, or other personal information that only the legitimate person should know. The workflow includes an initial step that generates questions based on available data, followed by a final step that evaluates the applicant's answers and provides scoring results. This "in-wallet" authentication method helps verify identity by testing knowledge of information typically found in the applicant's credit file and financial records. Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `Idempotency-Key` | header | yes | A unique, idempotent key. We recommend to use a guid | | `Agc-Tenant-Id` | header | yes | The tenant ID provided by Agility Credit. | | `Agc-User-Id` | header | yes | The requestor user ID. | | `Agc-Applicant-Id` | header | | If passed, will assign the report to that applicant. If not, will create a new applicant ID | ## Request body (`application/json`), required Inquiry request payload. > ⚠️ **Data is automatically sanitized** before it is sent to the bureau — > disallowed characters are stripped, values are reformatted or truncated to > the bureau's limits, and values that cannot be made valid are dropped. Your > original values are still stored on the applicant record. See > [**Precise ID data sanitization**](https://apidocs.agilitycredit.com/data-sanitization/precise-id) for the > full per-field rules. Variant 1: `PreciseID` | Field | Type | Required | Description | | --- | --- | --- | --- | | `format` | string: `json`, `raw`, `xml` | | | | `applicant` | object | yes | | | `applicant.first_name` | string | yes | Applicant first name. Minimum entry is the first initial. Compound names should be separated with a hyphen or space. Allowable special characters: hyphen, space, apostrophe. | | `applicant.middle_name` | string | | Applicant middle name; a middle initial is acceptable. Blanks and special characters are not allowed. Omit if not available. | | `applicant.last_name` | string | yes | Applicant surname; must be two or more characters. Split double surnames with a hyphen (e.g. Smith-Jones). Apostrophe usage is limited to the letters D, L and O. Allowable special characters: hyphen, space, apostrophe. The full name (last … | | `applicant.suffix` | string: `JR`, `SR`, `II`, `III`, `IV`, `V`, `VI`, `VII`, `VIII`, `IX` | | Generation / name suffix code. | | `applicant.ssn` | string | yes | Social Security Number (required); 4, 9 or 11 characters. Allowable special characters: hyphen, space. | | `applicant.dob` | string | | Applicant date of birth in YYYY-MM-DD format (a bare YYYY is also accepted and treated as dob_year). Use dob when the full birth date is available; otherwise use dob_year. | | `applicant.dob_year` | string | | Applicant year of birth in YYYY format. Use when the full date of birth is not available. | | `applicant.age` | integer | | Applicant age. Domain 14 - 120. | | `applicant.phone_mobile` | string | | Applicant mobile telephone number; 10 digits (e.g. 5551234567). Formatting characters and a leading country-code 1 are removed automatically; the value must reduce to exactly 10 digits or it is dropped from the bureau request. | | `applicant.phone_home` | string | | Applicant home telephone number; 10 digits (e.g. 5551234567). Formatting characters and a leading country-code 1 are removed automatically; the value must reduce to exactly 10 digits or it is dropped from the bureau request. | | `applicant.phone_work` | string | | Applicant work telephone number; 10 digits (e.g. 5551234567). Formatting characters and a leading country-code 1 are removed automatically; the value must reduce to exactly 10 digits or it is dropped from the bureau request. | | `applicant.emails` | array of string | | Up to 3 applicant email addresses. Allowable special characters: at sign, underscore, hyphen, period. | | `applicant.address` | object | yes | Applicant current address. The full address (street + city + state + zip) cannot exceed 118 characters. | | `applicant.address.po_box_number` | string | | PO Box number, included as part of the street line. | | `applicant.address.street1` | string | yes | Street address, PO Box or route number. Allowable special characters: forward slash, period, hyphen, pound sign, space. | | `applicant.address.street2` | string | | Additional street address line. | | `applicant.address.city` | string | yes | City name. Allowable special characters: apostrophe, period, hyphen, space. | | `applicant.address.state_code` | string | yes | Two-character state code. | | `applicant.address.zip` | string | yes | ZIP code. Allowable special characters: hyphen. | | `applicant.address.country_code` | string | | ISO country code. | | `applicant.address.time_at_address` | object | | Time at the current residence. Provide both years and months together — residence-duration reporting is omitted if only one is supplied. | | `applicant.address_previous` | object | | Applicant previous address (informational only). The full address (street + city + state + zip) cannot exceed 118 characters. | | `applicant.address_previous.po_box_number` | string | | PO Box number, included as part of the street line. | | `applicant.address_previous.street1` | string | yes | Street address, PO Box or route number. Allowable special characters: forward slash, period, hyphen, pound sign, space. | | `applicant.address_previous.street2` | string | | Additional street address line. | | `applicant.address_previous.city` | string | yes | City name. Allowable special characters: apostrophe, period, hyphen, space. | | `applicant.address_previous.state_code` | string | yes | Two-character state code. | | `applicant.address_previous.zip` | string | yes | ZIP code. Allowable special characters: hyphen. | | `applicant.address_previous.country_code` | string | | ISO country code. | | `applicant.address_previous.time_at_address` | object | | Time at the previous residence. Provide both years and months together — residence-duration reporting is omitted if only one is supplied. | | `applicant.employment` | object | | Applicant current employment. The full employment data (company + address + city + state + zip) cannot exceed 73 characters. | | `applicant.employment.employer_name` | string | | Company name; required when employment is supplied. Allowable special characters: pound sign, ampersand, hyphen, space. | | `applicant.employment.employer_address` | object | | | | `applicant.employment.income` | object | | | | `applicant.employment_previous` | object | | Applicant previous employment. | | `applicant.employment_previous.employer_name` | string | | Company name. Allowable special characters: pound sign, ampersand, hyphen, space. | | `applicant.employment_previous.employer_address` | object | | | | `applicant.employment_previous.income` | object | | | | `applicant.mothers_maiden_name` | string | | Applicant mother's maiden name. | | `applicant.spouse_name` | string | | Full name of the applicant's spouse. | | `applicant.no_of_dependents` | integer | | Total number of dependents, excluding the applicant. | | `applicant.driver_license` | object | | | | `applicant.driver_license.number` | string | | Driver's license number; required when driver_license is supplied. A minimum of two characters must be entered. Allowable special characters: space, asterisk, hyphen. | | `applicant.driver_license.state` | string | | Two-character driver's license state code; required when driver_license is supplied. | ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | Inquiry processed successfully | one of: PreciseIDResponseKIQInitial | | 400 | Bad Request | BadRequestResponse | | 401 | Unauthorized | UnauthorizedResponse | | 403 | Forbidden | UnauthorizedResponse | | 429 | Too Many Requests | TooManyRequestsResponse | | 500 | Internal Server Error | AgilityServerErrorResponse | The 200 body is one of: `PreciseIDResponseKIQInitial`. ### Error bodies 400: ```json { "status": "Bad Request - Invalid Parameters", "error": "Failed schema validation. Error: data.pull_type must be one of ['prequal', 'hard'], Path: ['data', 'pull_type'], Data: prequalification" } ``` 401: ```json { "message": "Unauthorized" } ``` 403: ```json { "message": "Unauthorized" } ``` 429: ```json { "error": "Too Many Requests" } ``` 500: ```json { "message": "Agility Server Error" } ``` --- # Out-of-Wallet Final `POST https://api.agilitycredit.net/v3/precise-id/oow/{session_id}` Docs page: https://apidocs.agilitycredit.com/api/precise-id-oow-final · Tag: Precise ID The "Out of Wallet Questions - Final request" endpoint performs identity verification through knowledge-based questions derived from the applicant's credit history and public records. This service presents multiple-choice questions about the applicant's financial history, previous addresses, loans, or other personal information that only the legitimate person should know. The workflow includes an initial step that generates questions based on available data, followed by a final step that evaluates the applicant's answers and provides scoring results. This "in-wallet" authentication method helps verify identity by testing knowledge of information typically found in the applicant's credit file and financial records. Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `session_id` | path | yes | The session identifier returned from the corresponding initial request. | | `Idempotency-Key` | header | yes | A unique, idempotent key. We recommend to use a guid | | `Agc-Tenant-Id` | header | yes | The tenant ID provided by Agility Credit. | | `Agc-User-Id` | header | yes | The requestor user ID. | | `Agc-Applicant-Id` | header | | If passed, will assign the report to that applicant. If not, will create a new applicant ID | ## Request body (`application/json`), required Inquiry request payload Variant 1: `PreciseIDKIQFinal` | Field | Type | Required | Description | | --- | --- | --- | --- | | `answers` | array of object | yes | | | `answers[].answer` | string | | | ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | Inquiry processed successfully | one of: PreciseIDResponseKIQFinal | | 400 | Bad Request | BadRequestResponse | | 401 | Unauthorized | UnauthorizedResponse | | 403 | Forbidden | UnauthorizedResponse | | 429 | Too Many Requests | TooManyRequestsResponse | | 500 | Internal Server Error | AgilityServerErrorResponse | The 200 body is one of: `PreciseIDResponseKIQFinal`. ### Error bodies 400: ```json { "status": "Bad Request - Invalid Parameters", "error": "Failed schema validation. Error: data.pull_type must be one of ['prequal', 'hard'], Path: ['data', 'pull_type'], Data: prequalification" } ``` 401: ```json { "message": "Unauthorized" } ``` 403: ```json { "message": "Unauthorized" } ``` 429: ```json { "error": "Too Many Requests" } ``` 500: ```json { "message": "Agility Server Error" } ``` --- # Get OTP Record `GET https://api.agilitycredit.net/v3/otp/{id}` Docs page: https://apidocs.agilitycredit.com/api/get-otp-record · Tag: Precise ID Retrieves an OTP (One-Time Passcode) verification record by ID. Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | The OTP record identifier. | | `Agc-Tenant-Id` | header | yes | The tenant ID provided by Agility Credit. | | `Agc-User-Id` | header | yes | The requestor user ID. | ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | OTP record retrieved successfully | OTPResponse | | 400 | Bad Request | StandardErrorResponse | | 401 | Unauthorized | StandardErrorResponse | | 429 | Too Many Requests | StandardErrorResponse | | 500 | Internal Server Error | StandardErrorResponse | Top-level 200 fields: `id`, `tenant_id`, `created_at`, `updated_at`, `otp_sent`, `decision`, `input`. --- # Get OOW Record `GET https://api.agilitycredit.net/v3/oow/{id}` Docs page: https://apidocs.agilitycredit.com/api/get-oow-record · Tag: Precise ID Retrieves an Out-of-Wallet (OOW) knowledge-based authentication record by ID. Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | The OOW record identifier. | | `Agc-Tenant-Id` | header | yes | The tenant ID provided by Agility Credit. | | `Agc-User-Id` | header | yes | The requestor user ID. | ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | OOW record retrieved successfully | OOWResponse | | 400 | Bad Request | StandardErrorResponse | | 401 | Unauthorized | StandardErrorResponse | | 429 | Too Many Requests | StandardErrorResponse | | 500 | Internal Server Error | StandardErrorResponse | Top-level 200 fields: `id`, `tenant_id`, `created_at`, `updated_at`, `score`, `decision`, `input`. --- # Auth Initial `POST https://api.agilitycredit.net/v3/prefill/auth` Docs page: https://apidocs.agilitycredit.com/api/prefill-auth-initial · Tag: Prefill Starts the possession step of the Prefill flow, confirming the applicant controls the supplied mobile number. Choose a channel via `type`: - `otp` — An SMS one-time passcode is sent to the applicant. Complete the step by submitting the code to **Auth Final** (`POST /prefill/auth/{session_id}`). - `magic_link` — An `auth_url` (a Magic Link) is returned. Present it to the applicant; after they tap it, **poll** Auth Final to read the result. The response carries the engine-minted `session_id` (echo it back on Auth Final as the `{session_id}` path parameter) and an `Agc-Applicant-Id` response header identifying the applicant record. Pass `Agc-Applicant-Id` back on later calls to update the same applicant instead of creating a new one. Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `Idempotency-Key` | header | yes | A unique, idempotent key. We recommend to use a guid | | `Agc-Tenant-Id` | header | yes | The tenant ID provided by Agility Credit. | | `Agc-User-Id` | header | yes | The requestor user ID. | | `Agc-Applicant-Id` | header | | If passed, will assign the report to that applicant. If not, will create a new applicant ID | ## Request body (`application/json`), required Possession initiation payload. Use the Magic Link or OTP variant. Variant 1: `ProveAuthInitialMagicLink` | Field | Type | Required | Description | | --- | --- | --- | --- | | `type` | string: `magic_link` | yes | Possession channel. Use `magic_link` for the Magic Link flow. | | `phone_mobile` | string | yes | The applicant's mobile phone number. | | `callback_url` | string (uri) | yes | Required when `type` is `magic_link`. HTTPS URL associated with the session. The applicant currently sees a standard Agility confirmation page after tapping the Magic Link; redirecting the browser back to this URL is planned for a future i… | | `secret` | string | | Optional shared secret. When supplied, responses for this session are signed with an `X-Agc-Signature` header (HMAC-SHA256 over `{timestamp}.{body}`), matching the Agility webhook signing convention. | | `dob` | string | | Optional applicant date of birth. Accepted formats: `YYYY-MM-DD` (full ISO-8601, e.g. `1990-01-31`) or `MM/YYYY` (month and year only, e.g. `01/1990`). | | `message_text` | string | | Optional custom prefix for the Magic Link SMS. The engine appends the auth URL after a colon: `"${message_text}: ${authUrl}"`. When omitted, the server auto-brands with the tenant name, e.g. `"Verify your identity with ExampleCU: https://.… | | `last4_ssn` | string | | Optional last four digits of the applicant's SSN. | Variant 2: `ProveAuthInitialOtp` | Field | Type | Required | Description | | --- | --- | --- | --- | | `type` | string: `otp` | yes | Possession channel. Use `otp` for the SMS one-time passcode flow. | | `phone_mobile` | string | yes | The applicant's mobile phone number. The passcode is sent here. | | `secret` | string | | Optional shared secret. When supplied, responses for this session are signed with an `X-Agc-Signature` header (HMAC-SHA256 over `{timestamp}.{body}`), matching the Agility webhook signing convention. | | `dob` | string | | Optional applicant date of birth. Accepted formats: `YYYY-MM-DD` (full ISO-8601, e.g. `1990-01-31`) or `MM/YYYY` (month and year only, e.g. `01/1990`). | | `message_text` | string | | Optional custom SMS body text. Prove substitutes the generated one-time passcode where `#####` appears. When omitted, the server uses a tenant-branded default (e.g. `"Your ExampleCU verification code is #####."`). Must include `#####` for … | | `last4_ssn` | string | | Optional last four digits of the applicant's SSN. | ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | Possession step initiated | one of: ProveAuthResponseInitialMagicLink, ProveAuthResponseInitialOtp | | 400 | Bad Request | BadRequestResponse | | 401 | Unauthorized | UnauthorizedResponse | | 403 | Forbidden | UnauthorizedResponse | | 429 | Too Many Requests | TooManyRequestsResponse | | 500 | Internal Server Error | AgilityServerErrorResponse | The 200 body is one of: `ProveAuthResponseInitialMagicLink`, `ProveAuthResponseInitialOtp`. ### Error bodies 400: ```json { "status": "Bad Request - Invalid Parameters", "error": "Failed schema validation. Error: data.pull_type must be one of ['prequal', 'hard'], Path: ['data', 'pull_type'], Data: prequalification" } ``` 401: ```json { "message": "Unauthorized" } ``` 403: ```json { "message": "Unauthorized" } ``` 429: ```json { "error": "Too Many Requests" } ``` 500: ```json { "message": "Agility Server Error" } ``` --- # Auth Final `POST https://api.agilitycredit.net/v3/prefill/auth/{session_id}` Docs page: https://apidocs.agilitycredit.com/api/prefill-auth-final · Tag: Prefill Completes or polls the possession step for the session identified by `{session_id}`. Behavior depends on the channel chosen at Auth Initial: - **OTP sessions** — submit the applicant's SMS code in `otp_code`. Returns a pass/fail (`passed`, `sms_status`). - **Magic Link sessions** — there is no code to submit; the applicant completes possession by tapping the Magic Link. Call this endpoint with an empty body to **poll** the result. `status` is `pending` until the applicant taps the link, then `verified` or `failed`. Poll until `completed` is `true`. > **Polling note:** this endpoint is a `POST` and responses are cached per `Idempotency-Key`. Send a **fresh** `Idempotency-Key` on each poll so you receive the latest status rather than a cached response. Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `session_id` | path | yes | The session identifier returned from the corresponding initial request. | | `Idempotency-Key` | header | yes | A unique, idempotent key. We recommend to use a guid | | `Agc-Tenant-Id` | header | yes | The tenant ID provided by Agility Credit. | | `Agc-User-Id` | header | yes | The requestor user ID. | | `Agc-Applicant-Id` | header | | If passed, will assign the report to that applicant. If not, will create a new applicant ID | ## Request body (`application/json`) OTP completion payload (`otp_code`) for OTP sessions, or an empty body when polling a Magic Link session. Variant 1: `ProveAuthFinal` | Field | Type | Required | Description | | --- | --- | --- | --- | | `otp_code` | string | | The one-time passcode the applicant received via SMS. Required for OTP sessions; omit when polling a Magic Link session. | | `secret` | string | | Optional shared secret. When it matches the secret supplied at Auth Initial, the response is signed with an `X-Agc-Signature` header. | ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | Possession status | one of: ProveAuthResponseOtpFinal, ProveAuthResponseMagicLinkStatus | | 400 | Bad Request | BadRequestResponse | | 401 | Unauthorized | UnauthorizedResponse | | 403 | Forbidden | UnauthorizedResponse | | 410 | Gone - the session expired or was not found. Restart with POST /prefill/auth. | BadRequestResponse | | 429 | Too Many Requests | TooManyRequestsResponse | | 500 | Internal Server Error | AgilityServerErrorResponse | The 200 body is one of: `ProveAuthResponseOtpFinal`, `ProveAuthResponseMagicLinkStatus`. ### Error bodies 400: ```json { "status": "Bad Request - Invalid Parameters", "error": "Failed schema validation. Error: data.pull_type must be one of ['prequal', 'hard'], Path: ['data', 'pull_type'], Data: prequalification" } ``` 401: ```json { "message": "Unauthorized" } ``` 403: ```json { "message": "Unauthorized" } ``` 410: ```json { "status": "Bad Request - Invalid Parameters", "error": "Failed schema validation. Error: data.pull_type must be one of ['prequal', 'hard'], Path: ['data', 'pull_type'], Data: prequalification" } ``` 429: ```json { "error": "Too Many Requests" } ``` 500: ```json { "message": "Agility Server Error" } ``` --- # Trust + Identity `POST https://api.agilitycredit.net/v3/prefill/trust-and-identity` Docs page: https://apidocs.agilitycredit.com/api/prefill-trust-and-identity · Tag: Prefill Runs the **Trust Score** and, when it clears the threshold and none of the optional `abandon_prefill_reason_codes` matched, the **Identity Prefill** — in a single call. Identify the session with the `Agc-Session-Id` header returned from `POST /prefill/auth`. When trust fails, or a caller abandon code matches, the response stops after trust with `proceeded_to_prefill: false` and `completed: true`. On success, `identity` is returned and `completed` is `false` (Verify Standalone may follow). > **Note:** this endpoint runs billable identity verification calls. Send a **fresh** `Idempotency-Key` per attempt. Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `Idempotency-Key` | header | yes | A unique, idempotent key. We recommend to use a guid | | `Agc-Tenant-Id` | header | yes | The tenant ID provided by Agility Credit. | | `Agc-User-Id` | header | yes | The requestor user ID. | | `Agc-Session-Id` | header | yes | The prefill session identifier returned from Auth Initial (POST /prefill/auth). | ## Request body (`application/json`) Optional prefill options (abandon codes / secret). Send `{}` to run with defaults. Schema: `ProvePrefillRequest` | Field | Type | Required | Description | | --- | --- | --- | --- | | `abandon_prefill_reason_codes` | array of string | | Trust reason codes that should stop the flow before prefill runs. If any code returned by the trust score is in this list, prefill is skipped and the trust result is returned with `proceeded_to_prefill: false`. Common examples: `NO`, `LP`,… | | `secret` | string | | Optional shared secret. When it matches the secret supplied at Auth Initial, the response is signed with an `X-Agc-Signature` header. | ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | Trust result, plus prefill identity when the flow proceeded | ProvePrefillResponse | | 400 | Bad Request | BadRequestResponse | | 401 | Unauthorized | UnauthorizedResponse | | 403 | Forbidden | UnauthorizedResponse | | 410 | Gone - the session expired or was not found. Restart with POST /prefill/auth. | BadRequestResponse | | 429 | Too Many Requests | TooManyRequestsResponse | | 500 | Internal Server Error | AgilityServerErrorResponse | Top-level 200 fields: `session_id`, `trust_score`, `trust_passed`, `trust_threshold`, `trust_reason_codes`, `reason_code_details`, `proceeded_to_prefill`, `abandoned_reason_codes`, `identity`, `identity_challenge_required`, `completed`. ### Error bodies 400: ```json { "status": "Bad Request - Invalid Parameters", "error": "Failed schema validation. Error: data.pull_type must be one of ['prequal', 'hard'], Path: ['data', 'pull_type'], Data: prequalification" } ``` 401: ```json { "message": "Unauthorized" } ``` 403: ```json { "message": "Unauthorized" } ``` 410: ```json { "status": "Bad Request - Invalid Parameters", "error": "Failed schema validation. Error: data.pull_type must be one of ['prequal', 'hard'], Path: ['data', 'pull_type'], Data: prequalification" } ``` 429: ```json { "error": "Too Many Requests" } ``` 500: ```json { "message": "Agility Server Error" } ``` --- # Identity Verify `POST https://api.agilitycredit.net/v3/prefill/verify` Docs page: https://apidocs.agilitycredit.com/api/identity-verify · Tag: Prefill Pure Identity Verify — verifies the identity submitted for a credit application against the prefill. Requires a prior possession session supplied via the `Agc-Session-Id` header (obtained from `POST /prefill/auth`). The engine **Identity Verify** only runs when the submitted identity differs from the prefill (email is not compared) — if everything matches, the prefill is trusted as-is (`changed: false`) and no additional engine call is made. Requires that `POST /prefill/trust-and-identity` has already run on the session. No trust fields are returned — this endpoint performs identity verification only. Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `Idempotency-Key` | header | yes | A unique, idempotent key. We recommend to use a guid | | `Agc-Tenant-Id` | header | yes | The tenant ID provided by Agility Credit. | | `Agc-User-Id` | header | yes | The requestor user ID. | | `Agc-Session-Id` | header | yes | The prefill session identifier returned from Auth Initial (POST /prefill/auth). | ## Request body (`application/json`), required The identity being submitted for the credit application. Schema: `ProveVerifyRequest` | Field | Type | Required | Description | | --- | --- | --- | --- | | `first_name` | string | yes | | | `last_name` | string | yes | | | `address` | string | | | | `extended_address` | string | | | | `city` | string | | | | `state` | string | | | | `zip` | string | | | | `dob` | string | | | | `ssn` | string | | Full SSN. Alternatively send `last4_ssn`. | | `last4_ssn` | string | | Last four of the SSN, if not sending the full `ssn`. | | `secret` | string | | Optional shared secret. When it matches the secret supplied at Auth Initial, the response is signed with an `X-Agc-Signature` header. | ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | Identity verify decision (or a no-change confirmation) | IdentityVerifyResponse | | 400 | Bad Request | BadRequestResponse | | 401 | Unauthorized | UnauthorizedResponse | | 403 | Forbidden | UnauthorizedResponse | | 409 | Conflict - no prefill on this session. Run POST /prefill/trust-and-identity before verifying. | BadRequestResponse | | 410 | Gone - the session expired or was not found. Restart with POST /prefill/auth. | BadRequestResponse | | 429 | Too Many Requests | TooManyRequestsResponse | | 500 | Internal Server Error | AgilityServerErrorResponse | Top-level 200 fields: `session_id`, `status`, `verify_passed`, `changed`, `verify_result`, `reason_code_details`, `completed`. ### Error bodies 400: ```json { "status": "Bad Request - Invalid Parameters", "error": "Failed schema validation. Error: data.pull_type must be one of ['prequal', 'hard'], Path: ['data', 'pull_type'], Data: prequalification" } ``` 401: ```json { "message": "Unauthorized" } ``` 403: ```json { "message": "Unauthorized" } ``` 409: ```json { "status": "Bad Request - Invalid Parameters", "error": "Failed schema validation. Error: data.pull_type must be one of ['prequal', 'hard'], Path: ['data', 'pull_type'], Data: prequalification" } ``` 410: ```json { "status": "Bad Request - Invalid Parameters", "error": "Failed schema validation. Error: data.pull_type must be one of ['prequal', 'hard'], Path: ['data', 'pull_type'], Data: prequalification" } ``` 429: ```json { "error": "Too Many Requests" } ``` 500: ```json { "message": "Agility Server Error" } ``` --- # Identity Verify Plus `POST https://api.agilitycredit.net/v3/verify-id` Docs page: https://apidocs.agilitycredit.com/api/idv-plus · Tag: Compliance Identity Verify Plus (IDV+) — runs trust scoring and identity verification in a single call. No prior possession or session required. Submit phone number and identity data directly in the request body. The engine performs the trust score and identity verify in one round trip and returns the combined result. Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `Idempotency-Key` | header | yes | A unique, idempotent key. We recommend to use a guid | | `Agc-Tenant-Id` | header | yes | The tenant ID provided by Agility Credit. | | `Agc-User-Id` | header | yes | The requestor user ID. | | `Agc-Applicant-Id` | header | | If passed, will assign the report to that applicant. If not, will create a new applicant ID | ## Request body (`application/json`), required Phone number and identity data for the IDV+ call. Schema: `IdvPlusRequest` | Field | Type | Required | Description | | --- | --- | --- | --- | | `phone_number` | string | yes | Applicant mobile phone number. Used for both trust scoring and identity lookup. | | `first_name` | string | yes | | | `last_name` | string | yes | | | `address` | string | | | | `extended_address` | string | | | | `city` | string | | | | `state` | string | | | | `zip` | string | | | | `dob` | string | | Date of birth in ISO 8601 format (YYYY-MM-DD) or year only (YYYY). | | `ssn` | string | | Full SSN. Alternatively send `last4_ssn`. | | `last4_ssn` | string | | Last four of the SSN, if not sending the full `ssn`. | | `applicant_id` | string | | Optional. If passed, associates the IDV+ result with an existing applicant. If omitted, a new applicant is created and returned in the `Agc-Applicant-Id` header. | ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | IDV+ result — trust score and identity verify decision | ProveVerifyResponse | | 400 | Bad Request | BadRequestResponse | | 401 | Unauthorized | UnauthorizedResponse | | 403 | Forbidden | UnauthorizedResponse | | 429 | Too Many Requests | TooManyRequestsResponse | | 500 | Internal Server Error | AgilityServerErrorResponse | Top-level 200 fields: `session_id`, `trust_score`, `trust_passed`, `trust_threshold`, `trust_reason_codes`, `status`, `verify_passed`, `changed`, `verify_result`, `reason_code_details`, `completed`. ### Error bodies 400: ```json { "status": "Bad Request - Invalid Parameters", "error": "Failed schema validation. Error: data.pull_type must be one of ['prequal', 'hard'], Path: ['data', 'pull_type'], Data: prequalification" } ``` 401: ```json { "message": "Unauthorized" } ``` 403: ```json { "message": "Unauthorized" } ``` 429: ```json { "error": "Too Many Requests" } ``` 500: ```json { "message": "Agility Server Error" } ``` --- # DL Verify `POST https://api.agilitycredit.net/v3/dlv` Docs page: https://apidocs.agilitycredit.com/api/run-dlv · Tag: DL Verify This endpoint allows for the validation of a driver's license, ID card, or military ID using Agility's verification service. It accepts an image or scanned data of the license ID and returns validation results, including authenticity checks and extracted information. A selfie can be included in the verification to match the applicant's face against the license photo and check liveness, in one of two ways: - **Upload the selfie**: send it as a base64 encoded image with type `selfie`, together with the license images or in a later request of the same session. - **Selfie URL**: send `return_selfie_url: true` with the license images. The response contains a `selfie_url` for the applicant to take their selfie. Once they have, send `end_session: true` with the same `session_id` to get the results. If the selfie is still being processed, the response is a `409` with a `Retry-After` header; retry the same request after that many seconds. When the DL Verify Document Liveness add-on is enabled, every verification also checks that the front image is a live capture of a physical document, not a photo of a screen, a printed copy, or a document with an altered portrait. The result is returned under `details.document_liveness`. A failed check adds `document_liveness` to `failed_checks` and the decision becomes `refer`. Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `Idempotency-Key` | header | yes | A unique, idempotent key. We recommend to use a guid | | `Agc-Tenant-Id` | header | yes | The tenant ID provided by Agility Credit. | | `Agc-User-Id` | header | yes | The requestor user ID. | | `Agc-Applicant-Id` | header | | If passed, will assign the report to that applicant. If not, will create a new applicant ID | ## Request body (`application/json`), required Inquiry request payload Schema: `DLVerify` | Field | Type | Required | Description | | --- | --- | --- | --- | | `session_id` | string | | The session ID returned from the DL Verify Endpoint. Pass it back to continue uploading images. | | `end_session` | boolean | | Set to true when its your last image upload. | | `selfie` | boolean | | Set to true in the first request of a session to include a selfie (facial match and liveness check) in the verification, when the selfie image is uploaded in a later request. Implied when a `selfie` image or `return_selfie_url` is sent. Re… | | `return_selfie_url` | boolean | | Set to true to receive a `selfie_url` the applicant can open to take their selfie. Cannot be combined with `end_session` or a `selfie` image in the same request. Once the applicant has taken the selfie, send `end_session` with the same `se… | | `images` | array of object | | | | `images[].data` | string | yes | Base64 encoded image of the driver's license or selfie. For `barcode`, the base64 encoded barcode data. | | `images[].type` | any: `front`, `back`, `barcode`, `selfie` | yes | | ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | Inquiry processed successfully | one of: DLResponse, DLResponseWithError | | 400 | Bad Request | BadRequestResponse | | 401 | Unauthorized | UnauthorizedResponse | | 403 | Product Not Enabled | object | | 404 | Session Not Found. The `session_id` does not exist or has expired; start a new session and submit all images again. | DLResponse | | 409 | Selfie Processing. The selfie taken on the `selfie_url` is still being processed. Retry the same request with the same `session_id` after the number of seconds in the `Retry-After` header. | DLResponse | | 429 | Too Many Requests | TooManyRequestsResponse | | 500 | Internal Server Error | AgilityServerErrorResponse | | 502 | Verification Error. The verification could not be processed; `errors` describes what failed. Retry with the same `session_id`. | DLResponse | The 200 body is one of: `DLResponse`, `DLResponseWithError`. ### Error bodies 400: ```json { "status": "Bad Request - Invalid Parameters", "error": "Failed schema validation. Error: data.pull_type must be one of ['prequal', 'hard'], Path: ['data', 'pull_type'], Data: prequalification" } ``` 401: ```json { "message": "Unauthorized" } ``` 429: ```json { "error": "Too Many Requests" } ``` 500: ```json { "message": "Agility Server Error" } ``` --- # DL Verify Results `GET https://api.agilitycredit.net/v3/dlv/{id}` Docs page: https://apidocs.agilitycredit.com/api/get-dlv-docs · Tag: DL Verify This endpoint returns all DL Verify data for an ID. This includes result and image URLs. Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | ID for DL Verify | | `Idempotency-Key` | header | yes | A unique, idempotent key. We recommend to use a guid | | `Agc-Tenant-Id` | header | yes | The tenant ID provided by Agility Credit. | | `Agc-User-Id` | header | yes | The requestor user ID. | ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | DL Verify documents request processed successfully | DLVerifyDocsResponse | | 401 | Unauthorized | UnauthorizedResponse | | 429 | Too Many Requests | TooManyRequestsResponse | | 500 | Internal Server Error | AgilityServerErrorResponse | Top-level 200 fields: `created_at`, `updated_at`, `result`, `images`, `id`, `tenant_id`. ### Error bodies 401: ```json { "message": "Unauthorized" } ``` 429: ```json { "error": "Too Many Requests" } ``` 500: ```json { "message": "Agility Server Error" } ``` --- # DL Verify Link `POST https://api.agilitycredit.net/v3/links/dlv` Docs page: https://apidocs.agilitycredit.com/api/create-dlv-link · Tag: Links This endpoint allows for the validation of a driver's license, ID card, or military ID using Agility's verification service by sending an email or a text message and returning a link. Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `Idempotency-Key` | header | yes | A unique, idempotent key. We recommend to use a guid | | `Agc-Tenant-Id` | header | yes | The tenant ID provided by Agility Credit. | | `Agc-User-Id` | header | yes | The requestor user ID. | ## Request body (`application/json`) DL Verify Link request payload Schema: `DLVerifyLink` | Field | Type | Required | Description | | --- | --- | --- | --- | | `phone` | string | | The phone number of the applicant in E.164 format. | | `email` | string (email) | | The email of the applicant in RFC 5322 format. | | `applicant_name` | string | | The name of the applicant. | ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | DL Verify Link processed successfully | DLVerifyLinkResponse | | 400 | Bad Request | BadRequestResponseDLVerifyLink | | 401 | Unauthorized | UnauthorizedResponse | | 429 | Too Many Requests | TooManyRequestsResponse | | 500 | Internal Server Error | AgilityServerErrorResponse | Top-level 200 fields: `dlv_link`. ### Error bodies 400: ```json { "status": "Bad Request - Validation Failed", "error": "Failed schema validation. Error: data.phone must match pattern ^\\+[1-9]\\d{1,14}$, Path: ['data', 'phone'], Data: 1234567890" } ``` 401: ```json { "message": "Unauthorized" } ``` 429: ```json { "error": "Too Many Requests" } ``` 500: ```json { "message": "Agility Server Error" } ``` --- # FastApp Link `POST https://api.agilitycredit.net/v3/links/fastapp` Docs page: https://apidocs.agilitycredit.com/api/fast-app-link · Tag: Links Creates a FastApp link for an applicant. The link can be delivered via email or SMS and directs the applicant to a pre-filled application form. Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `Idempotency-Key` | header | yes | A unique, idempotent key. We recommend to use a guid | | `Agc-Tenant-Id` | header | yes | The tenant ID provided by Agility Credit. | | `Agc-User-Id` | header | yes | The requestor user ID. | | `Agc-Applicant-Id` | header | | If passed, will assign the report to that applicant. If not, will create a new applicant ID | ## Request body (`application/json`), required FastApp link request payload | Field | Type | Required | Description | | --- | --- | --- | --- | | `fastapp_id` | string | yes | The FastApp configuration identifier. | | `phone` | string | | Phone number to send the FastApp link to. | | `email` | string | | Email address to send the FastApp link to. | | `applicant_type` | string | | The applicant type (e.g. main, co). | | `applicant_name` | string | | The applicant's display name for the link. | ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | FastApp link created successfully | FastAppLinkResponse | | 400 | Bad Request | StandardErrorResponse | | 401 | Unauthorized | StandardErrorResponse | | 429 | Too Many Requests | StandardErrorResponse | | 500 | Internal Server Error | StandardErrorResponse | Top-level 200 fields: `fastapp_link`. --- # Compliance Link `POST https://api.agilitycredit.net/v3/links/compliance` Docs page: https://apidocs.agilitycredit.com/api/compliance-link · Tag: Links Creates a compliance review link for an applicant. This triggers the compliance workflow and returns the compliance result. Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `Idempotency-Key` | header | yes | A unique, idempotent key. We recommend to use a guid | | `Agc-Tenant-Id` | header | yes | The tenant ID provided by Agility Credit. | | `Agc-User-Id` | header | yes | The requestor user ID. | | `Agc-Applicant-Id` | header | | If passed, will assign the report to that applicant. If not, will create a new applicant ID | ## Request body (`application/json`) Compliance link request payload ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | Compliance link processed successfully | ComplianceResponse | | 400 | Bad Request | StandardErrorResponse | | 401 | Unauthorized | StandardErrorResponse | | 429 | Too Many Requests | StandardErrorResponse | | 500 | Internal Server Error | StandardErrorResponse | Top-level 200 fields: `body_type`, `compliance_link`, `compliance_data`, `message`. --- # Get Applicant `GET https://api.agilitycredit.net/v3/applicant` Docs page: https://apidocs.agilitycredit.com/api/get-applicant · Tag: Applicants Retrieves an applicant record by applicant ID, including the primary and co-applicant inquiry data. Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `Agc-Tenant-Id` | header | yes | The tenant ID provided by Agility Credit. | | `Agc-User-Id` | header | yes | The requestor user ID. | | `Agc-Applicant-Id` | header | | If passed, will assign the report to that applicant. If not, will create a new applicant ID | ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | Applicant record retrieved successfully | ApplicantResponse | | 400 | Bad Request | StandardErrorResponse | | 401 | Unauthorized | StandardErrorResponse | | 429 | Too Many Requests | StandardErrorResponse | | 500 | Internal Server Error | StandardErrorResponse | Top-level 200 fields: `applicant_id`, `created_at`, `updated_at`, `applicant_main`, `applicant_co`. --- # Get Compliance Records `GET https://api.agilitycredit.net/v3/compliance` Docs page: https://apidocs.agilitycredit.com/api/get-compliance-all · Tag: Compliance This endpoint allows users to retrieve all compliance data corresponding to both main and co-applicant. Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `Idempotency-Key` | header | yes | A unique, idempotent key. We recommend to use a guid | | `Agc-Tenant-Id` | header | yes | The tenant ID provided by Agility Credit. | | `Agc-User-Id` | header | yes | The requestor user ID. | | `Agc-Applicant-Id` | header | yes | Applicant ID - this cannot be empty | ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | Compliance data request processed successfully. | ComplianceAllResponse | | 400 | Bad Request | BadRequestApplicantResponse | | 401 | Unauthorized | UnauthorizedResponse | | 429 | Too Many Requests | TooManyRequestsResponse | | 500 | Internal Server Error | AgilityServerErrorResponse | Top-level 200 fields: `applicant_main`, `applicant_co`. ### Error bodies 400: ```json { "status": "Bad Request - Validation Failed", "error": "Header must contain non-empty agc-applicant-id" } ``` 401: ```json { "message": "Unauthorized" } ``` 429: ```json { "error": "Too Many Requests" } ``` 500: ```json { "message": "Agility Server Error" } ``` --- # Get Compliance Record by Type `GET https://api.agilitycredit.net/v3/compliance/{applicant_type}` Docs page: https://apidocs.agilitycredit.com/api/get-compliance-single · Tag: Compliance This endpoint allows users to retrieve all compliance data corresponding to an applicant. Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `applicant_type` | path | yes | Applicant type | | `Idempotency-Key` | header | yes | A unique, idempotent key. We recommend to use a guid | | `Agc-Tenant-Id` | header | yes | The tenant ID provided by Agility Credit. | | `Agc-User-Id` | header | yes | The requestor user ID. | | `Agc-Applicant-Id` | header | yes | Applicant ID - this cannot be empty | ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | Compliance data request processed successfully. | ComplianceSingleResponse | | 400 | Bad Request | BadRequestComplianceResponse | | 401 | Unauthorized | UnauthorizedResponse | | 429 | Too Many Requests | TooManyRequestsResponse | | 500 | Internal Server Error | AgilityServerErrorResponse | Top-level 200 fields: `created_at`, `updated_at`, `synthetic_id`, `cleared`, `applicant_id`, `applicant_type`, `services`, `info_alerts`, `alerts`. ### Error bodies 400: ```json { "status": "Bad Request - Validation Failed", "error": "Failed schema validation. Error: data.applicant_type must be one of ['main', 'co'], Path: ['data', 'applicant_type'], Data: joint" } ``` 401: ```json { "message": "Unauthorized" } ``` 429: ```json { "error": "Too Many Requests" } ``` 500: ```json { "message": "Agility Server Error" } ``` --- # List Applicant Documents `GET https://api.agilitycredit.net/v3/applicant-documents` Docs page: https://apidocs.agilitycredit.com/api/list-applicant-documents · Tag: Documents This endpoint returns all documents for an applicant. Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `limit` | query | | Number of documents to return - defaults to 10. | | `cursor` | query | | Last key from previous query to fetch the next limit documents. | | `Idempotency-Key` | header | yes | A unique, idempotent key. We recommend to use a guid | | `Agc-Tenant-Id` | header | yes | The tenant ID provided by Agility Credit. | | `Agc-User-Id` | header | yes | The requestor user ID. | | `Agc-Applicant-Id` | header | yes | Applicant ID - this cannot be empty | ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | Applicant document request processed successfully | AppDocAllResponse | | 401 | Unauthorized | UnauthorizedResponse | | 429 | Too Many Requests | TooManyRequestsResponse | | 500 | Internal Server Error | AgilityServerErrorResponse | Top-level 200 fields: `items`, `cursor`. ### Error bodies 401: ```json { "message": "Unauthorized" } ``` 429: ```json { "error": "Too Many Requests" } ``` 500: ```json { "message": "Agility Server Error" } ``` --- # Get Applicant Document `GET https://api.agilitycredit.net/v3/applicant-documents/{file_id}` Docs page: https://apidocs.agilitycredit.com/api/get-applicant-document · Tag: Documents This endpoint returns applicant document using a file ID. Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `file_id` | path | yes | File ID for the applicant | | `Idempotency-Key` | header | yes | A unique, idempotent key. We recommend to use a guid | | `Agc-Tenant-Id` | header | yes | The tenant ID provided by Agility Credit. | | `Agc-User-Id` | header | yes | The requestor user ID. | | `Agc-Applicant-Id` | header | yes | Applicant ID - this cannot be empty | ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | Applicant document request processed successfully | AppDocResponse | | 401 | Unauthorized | UnauthorizedResponse | | 429 | Too Many Requests | TooManyRequestsResponse | | 500 | Internal Server Error | AgilityServerErrorResponse | Top-level 200 fields: `created_at`, `updated_at`, `id`, `filename`, `category`, `mime_type`, `size`, `download_url`. ### Error bodies 401: ```json { "message": "Unauthorized" } ``` 429: ```json { "error": "Too Many Requests" } ``` 500: ```json { "message": "Agility Server Error" } ``` --- # OFAC Check `POST https://api.agilitycredit.net/v3/ofac` Docs page: https://apidocs.agilitycredit.com/api/run-ofac · Tag: Compliance This endpoint allows users to perform an OFAC (Office of Foreign Assets Control) check on an applicant or a business to ensure compliance with U.S. regulations. By submitting the necessary applicant or entity information, users can verify if the applicant or entity is listed on any OFAC sanctions lists. The response will indicate whether the applicant or entity passes the OFAC check or if there are any matches found. For screening entities, only the business name is required. Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `Idempotency-Key` | header | yes | A unique, idempotent key. We recommend to use a guid | | `Agc-Tenant-Id` | header | yes | The tenant ID provided by Agility Credit. | | `Agc-User-Id` | header | yes | The requestor user ID. | | `Agc-Applicant-Id` | header | | If passed, will assign the report to that applicant. If not, will create a new applicant ID | ## Request body (`application/json`), required OFAC check request payload Schema: `OFACRequest` Variant 1: `Individual` | Field | Type | Required | Description | | --- | --- | --- | --- | | `applicant_main` | object | yes | | | `applicant_main.first_name` | string | yes | The first name of the applicant. [REQUIRED] | | `applicant_main.middle_name` | string | | The middle name of the applicant. Middle name can be an initial. | | `applicant_main.last_name` | string | yes | The last name of the applicant. [REQUIRED] | | `applicant_main.ssn` | string | | The SSN of the applicant. | | `applicant_main.dob` | string | yes | The date of birth of the applicant. [REQUIRED] unless soft_check is true. | | `applicant_main.phone` | string | | The phone number of the applicant. | | `applicant_main.email` | string | | The email address of the applicant. | | `applicant_main.address` | string | | The address of the applicant. The format is '{house_number} {street_name} {street_type} {direction}'. street_type and direction are optional. | | `applicant_main.zip` | string | | The ZIP code of the applicant. | | `applicant_main.state` | string | | The state of the applicant. | | `applicant_main.city` | string | | The city of the applicant. Abbreviation of the city name. E.g 'NY'. | | `applicant_co` | object | | | | `applicant_co.first_name` | string | yes | The first name of the applicant. [REQUIRED] | | `applicant_co.middle_name` | string | | The middle name of the applicant. Middle name can be an initial. | | `applicant_co.last_name` | string | yes | The last name of the applicant. [REQUIRED] | | `applicant_co.ssn` | string | | The SSN of the applicant. | | `applicant_co.dob` | string | yes | The date of birth of the applicant. [REQUIRED] unless soft_check is true. | | `applicant_co.phone` | string | | The phone number of the applicant. | | `applicant_co.email` | string | | The email address of the applicant. | | `applicant_co.address` | string | | The address of the applicant. The format is '{house_number} {street_name} {street_type} {direction}'. street_type and direction are optional. | | `applicant_co.zip` | string | | The ZIP code of the applicant. | | `applicant_co.state` | string | | The state of the applicant. | | `applicant_co.city` | string | | The city of the applicant. Abbreviation of the city name. E.g 'NY'. | | `soft_check` | boolean | | If true, then dob is not required inside applicant_main and applicant_co. | Variant 2: `Entity` | Field | Type | Required | Description | | --- | --- | --- | --- | | `business_main` | object | yes | | | `business_main.business_name` | string | yes | Name of the business for screening entities. [REQUIRED] | Example: ```json { "applicant_main": { "first_name": "JANE", "last_name": "SAMPLE", "phone": "", "email": "", "dob": "1901-01-01", "address": "123 MAIN ST", "city": "ANYTOWN", "state": "TX", "zip": "750010000" }, "applicant_co": { "first_name": "JANE", "last_name": "DOE", "phone": "", "email": "", "dob": "1980-01-01", "address": "123 MAIN ST", "city": "ANYTOWN", "state": "TX", "zip": "750010000" } } ``` ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | OFAC check processed successfully | one of: IndividualResponse, EntityResponse | | 400 | Bad Request | BadRequestResponseOFAC | | 401 | Unauthorized | UnauthorizedResponse | | 429 | Too Many Requests | TooManyRequestsResponse | | 500 | Internal Server Error | AgilityServerErrorResponse | The 200 body is one of: `IndividualResponse`, `EntityResponse`. ### Error bodies 400: ```json { "status": "Bad Request - Validation Failed", "error": "Failed schema validation. Error: data.applicant_main must contain ['dob'] properties, Path: ['data', 'applicant_main'], Data: {'first_name': 'Jimmy', 'middle_name': 'H', 'last_name': 'BRIDGES', 'phone': '', 'email': '', 'address': '217 PINEWOOD DR', 'city': 'EUSTIS', 'state': 'FL', 'zip': '327267427'}" } ``` 401: ```json { "message": "Unauthorized" } ``` 429: ```json { "error": "Too Many Requests" } ``` 500: ```json { "message": "Agility Server Error" } ``` --- # MLA Check `POST https://api.agilitycredit.net/v3/mla` Docs page: https://apidocs.agilitycredit.com/api/run-mla · Tag: MLA This endpoint allows users to perform a Military Lending Act (MLA) check on an applicant to ensure compliance with MLA regulations. By submitting the necessary applicant information, users can verify if the applicant is covered under the MLA and receive relevant compliance information. Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `Idempotency-Key` | header | yes | A unique, idempotent key. We recommend to use a guid | | `Agc-Tenant-Id` | header | yes | The tenant ID provided by Agility Credit. | | `Agc-User-Id` | header | yes | The requestor user ID. | | `Agc-Applicant-Id` | header | | If passed, will assign the report to that applicant. If not, will create a new applicant ID | ## Request body (`application/json`), required MLA check request payload Schema: `MLAInquiry` | Field | Type | Required | Description | | --- | --- | --- | --- | | `applicant_main` | object | yes | | | `applicant_main.first_name` | string | yes | The first name of the applicant. [REQUIRED] | | `applicant_main.middle_name` | string | | The middle name of the applicant. Middle name can be an initial. | | `applicant_main.last_name` | string | yes | The last name of the applicant. [REQUIRED] | | `applicant_main.ssn` | string | | The SSN of the applicant. Required if pull_type is set to 'hard'. | | `applicant_main.dob` | string | | The date of birth of the applicant. Required if pull_type is set to 'hard'. | | `applicant_main.phone` | string | | The phone number of the applicant. | | `applicant_main.email` | string | | The email address of the applicant. | | `applicant_main.address` | string | yes | The address of the applicant. The format is '{house_number} {street_name} {street_type} {direction}'. street_type and direction are optional. [REQUIRED] | | `applicant_main.zip` | string | yes | The ZIP code of the applicant. [REQUIRED] | | `applicant_main.state` | string | yes | The state of the applicant. [REQUIRED] | | `applicant_main.city` | string | yes | The city of the applicant. Abbreviation of the city name. E.g 'NY'. [REQUIRED] | | `applicant_main.income` | object | | | | `applicant_main.income.gross_monthly` | integer | yes | Gross monthly income, greater than zero. [REQUIRED] | | `applicant_main.income.other_monthly` | integer | | Other monthly income, greater than zero. | | `applicant_main.income.other_source` | string: `aid_for_dependent_children`, `child_support`, `cola`, `disability`, `fssa`, `housing_allowances`, `military_basic_allowance_for_subsistence`, `municipal_bond_interest`, `non_taxable_sources_of_income`, `other`, … (5 more) | | Source of other income. | | `applicant_co` | object | | | | `applicant_co.first_name` | string | yes | The first name of the applicant. [REQUIRED] | | `applicant_co.middle_name` | string | | The middle name of the applicant. Middle name can be an initial. | | `applicant_co.last_name` | string | yes | The last name of the applicant. [REQUIRED] | | `applicant_co.ssn` | string | | The SSN of the applicant. Required if pull_type is set to 'hard'. | | `applicant_co.dob` | string | | The date of birth of the applicant. Required if pull_type is set to 'hard'. | | `applicant_co.phone` | string | | The phone number of the applicant. | | `applicant_co.email` | string | | The email address of the applicant. | | `applicant_co.address` | string | yes | The address of the applicant. The format is '{house_number} {street_name} {street_type} {direction}'. street_type and direction are optional. [REQUIRED] | | `applicant_co.zip` | string | yes | The ZIP code of the applicant. [REQUIRED] | | `applicant_co.state` | string | yes | The state of the applicant. [REQUIRED] | | `applicant_co.city` | string | yes | The city of the applicant. Abbreviation of the city name. E.g 'NY'. [REQUIRED] | | `applicant_co.income` | object | | | | `applicant_co.income.gross_monthly` | integer | yes | Gross monthly income, greater than zero. [REQUIRED] | | `applicant_co.income.other_monthly` | integer | | Other monthly income, greater than zero. | | `applicant_co.income.other_source` | string: `aid_for_dependent_children`, `child_support`, `cola`, `disability`, `fssa`, `housing_allowances`, `military_basic_allowance_for_subsistence`, `municipal_bond_interest`, `non_taxable_sources_of_income`, `other`, … (5 more) | | Source of other income. | | `format` | string: `json`, `raw` | | | Example: ```json { "applicant_main": { "first_name": "JANE", "last_name": "SAMPLE", "phone": "", "email": "", "address": "123 MAIN ST", "city": "ANYTOWN", "state": "TX", "zip": "750010000" }, "applicant_co": { "first_name": "JANE", "last_name": "DOE", "phone": "", "email": "", "address": "123 MAIN ST", "city": "ANYTOWN", "state": "TX", "zip": "750010000" } } ``` ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | MLA check processed successfully | MLAResponse | | 400 | Bad Request | BadRequestResponse | | 401 | Unauthorized | UnauthorizedResponse | | 429 | Too Many Requests | TooManyRequestsResponse | | 500 | Internal Server Error | AgilityServerErrorResponse | Top-level 200 fields: `applicant_main`. ### Error bodies 400: ```json { "status": "Bad Request - Invalid Parameters", "error": "Failed schema validation. Error: data.pull_type must be one of ['prequal', 'hard'], Path: ['data', 'pull_type'], Data: prequalification" } ``` 401: ```json { "message": "Unauthorized" } ``` 429: ```json { "error": "Too Many Requests" } ``` 500: ```json { "message": "Agility Server Error" } ``` --- # Verify Plus `POST https://api.agilitycredit.net/v3/verify/plus` Docs page: https://apidocs.agilitycredit.com/api/verify-plus · Tag: Experian Verify This endpoint performs an Experian Verify Plus check on an applicant. Returns employment and income verification data from Experian's Verify service. Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `Idempotency-Key` | header | yes | A unique, idempotent key. We recommend to use a guid | | `Agc-Tenant-Id` | header | yes | The tenant ID provided by Agility Credit. | | `Agc-User-Id` | header | yes | The requestor user ID. | | `Agc-Applicant-Id` | header | | If passed, will assign the report to that applicant. If not, will create a new applicant ID | ## Request body (`application/json`), required Verify Plus request payload Schema: `ApplicantInquiry` | Field | Type | Required | Description | | --- | --- | --- | --- | | `applicant_main` | object | yes | | | `applicant_main.first_name` | string | yes | The first name of the applicant. [REQUIRED] | | `applicant_main.middle_name` | string | | The middle name of the applicant. Middle name can be an initial. | | `applicant_main.last_name` | string | yes | The last name of the applicant. [REQUIRED] | | `applicant_main.ssn` | string | | The SSN of the applicant. Required if pull_type is set to 'hard'. | | `applicant_main.dob` | string | | The date of birth of the applicant. Required if pull_type is set to 'hard'. | | `applicant_main.phone` | string | | The phone number of the applicant. | | `applicant_main.email` | string | | The email address of the applicant. | | `applicant_main.address` | string | yes | The address of the applicant. The format is '{house_number} {street_name} {street_type} {direction}'. street_type and direction are optional. [REQUIRED] | | `applicant_main.zip` | string | yes | The ZIP code of the applicant. [REQUIRED] | | `applicant_main.state` | string | yes | The state of the applicant. [REQUIRED] | | `applicant_main.city` | string | yes | The city of the applicant. Abbreviation of the city name. E.g 'NY'. [REQUIRED] | | `applicant_main.income` | object | | | | `applicant_main.income.gross_monthly` | integer | yes | Gross monthly income, greater than zero. [REQUIRED] | | `applicant_main.income.other_monthly` | integer | | Other monthly income, greater than zero. | | `applicant_main.income.other_source` | string: `aid_for_dependent_children`, `child_support`, `cola`, `disability`, `fssa`, `housing_allowances`, `military_basic_allowance_for_subsistence`, `municipal_bond_interest`, `non_taxable_sources_of_income`, `other`, … (5 more) | | Source of other income. | | `applicant_co` | object | | | | `applicant_co.first_name` | string | yes | The first name of the applicant. [REQUIRED] | | `applicant_co.middle_name` | string | | The middle name of the applicant. Middle name can be an initial. | | `applicant_co.last_name` | string | yes | The last name of the applicant. [REQUIRED] | | `applicant_co.ssn` | string | | The SSN of the applicant. Required if pull_type is set to 'hard'. | | `applicant_co.dob` | string | | The date of birth of the applicant. Required if pull_type is set to 'hard'. | | `applicant_co.phone` | string | | The phone number of the applicant. | | `applicant_co.email` | string | | The email address of the applicant. | | `applicant_co.address` | string | yes | The address of the applicant. The format is '{house_number} {street_name} {street_type} {direction}'. street_type and direction are optional. [REQUIRED] | | `applicant_co.zip` | string | yes | The ZIP code of the applicant. [REQUIRED] | | `applicant_co.state` | string | yes | The state of the applicant. [REQUIRED] | | `applicant_co.city` | string | yes | The city of the applicant. Abbreviation of the city name. E.g 'NY'. [REQUIRED] | | `applicant_co.income` | object | | | | `applicant_co.income.gross_monthly` | integer | yes | Gross monthly income, greater than zero. [REQUIRED] | | `applicant_co.income.other_monthly` | integer | | Other monthly income, greater than zero. | | `applicant_co.income.other_source` | string: `aid_for_dependent_children`, `child_support`, `cola`, `disability`, `fssa`, `housing_allowances`, `military_basic_allowance_for_subsistence`, `municipal_bond_interest`, `non_taxable_sources_of_income`, `other`, … (5 more) | | Source of other income. | Example: ```json { "applicant_main": { "first_name": "JANE", "last_name": "SAMPLE", "phone": "", "email": "", "address": "123 MAIN ST", "city": "ANYTOWN", "state": "TX", "zip": "750010000" }, "applicant_co": { "first_name": "JANE", "last_name": "DOE", "phone": "", "email": "", "address": "123 MAIN ST", "city": "ANYTOWN", "state": "TX", "zip": "750010000" } } ``` ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | Verify Plus processed successfully | ExperianVerifyResponse | | 400 | Bad Request | StandardErrorResponse | | 401 | Unauthorized | StandardErrorResponse | | 403 | Product Not Enabled | StandardErrorResponse | | 429 | Too Many Requests | StandardErrorResponse | | 500 | Internal Server Error | StandardErrorResponse | Top-level 200 fields: `pdf_s3_key`, `errors`. --- # Verify Premium `POST https://api.agilitycredit.net/v3/verify/premium` Docs page: https://apidocs.agilitycredit.com/api/verify-premium · Tag: Experian Verify This endpoint performs an Experian Verify Premium check on an applicant. Returns enhanced employment and income verification data from Experian's Verify service. Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `Idempotency-Key` | header | yes | A unique, idempotent key. We recommend to use a guid | | `Agc-Tenant-Id` | header | yes | The tenant ID provided by Agility Credit. | | `Agc-User-Id` | header | yes | The requestor user ID. | | `Agc-Applicant-Id` | header | | If passed, will assign the report to that applicant. If not, will create a new applicant ID | ## Request body (`application/json`), required Verify Premium request payload Schema: `ApplicantInquiry` | Field | Type | Required | Description | | --- | --- | --- | --- | | `applicant_main` | object | yes | | | `applicant_main.first_name` | string | yes | The first name of the applicant. [REQUIRED] | | `applicant_main.middle_name` | string | | The middle name of the applicant. Middle name can be an initial. | | `applicant_main.last_name` | string | yes | The last name of the applicant. [REQUIRED] | | `applicant_main.ssn` | string | | The SSN of the applicant. Required if pull_type is set to 'hard'. | | `applicant_main.dob` | string | | The date of birth of the applicant. Required if pull_type is set to 'hard'. | | `applicant_main.phone` | string | | The phone number of the applicant. | | `applicant_main.email` | string | | The email address of the applicant. | | `applicant_main.address` | string | yes | The address of the applicant. The format is '{house_number} {street_name} {street_type} {direction}'. street_type and direction are optional. [REQUIRED] | | `applicant_main.zip` | string | yes | The ZIP code of the applicant. [REQUIRED] | | `applicant_main.state` | string | yes | The state of the applicant. [REQUIRED] | | `applicant_main.city` | string | yes | The city of the applicant. Abbreviation of the city name. E.g 'NY'. [REQUIRED] | | `applicant_main.income` | object | | | | `applicant_main.income.gross_monthly` | integer | yes | Gross monthly income, greater than zero. [REQUIRED] | | `applicant_main.income.other_monthly` | integer | | Other monthly income, greater than zero. | | `applicant_main.income.other_source` | string: `aid_for_dependent_children`, `child_support`, `cola`, `disability`, `fssa`, `housing_allowances`, `military_basic_allowance_for_subsistence`, `municipal_bond_interest`, `non_taxable_sources_of_income`, `other`, … (5 more) | | Source of other income. | | `applicant_co` | object | | | | `applicant_co.first_name` | string | yes | The first name of the applicant. [REQUIRED] | | `applicant_co.middle_name` | string | | The middle name of the applicant. Middle name can be an initial. | | `applicant_co.last_name` | string | yes | The last name of the applicant. [REQUIRED] | | `applicant_co.ssn` | string | | The SSN of the applicant. Required if pull_type is set to 'hard'. | | `applicant_co.dob` | string | | The date of birth of the applicant. Required if pull_type is set to 'hard'. | | `applicant_co.phone` | string | | The phone number of the applicant. | | `applicant_co.email` | string | | The email address of the applicant. | | `applicant_co.address` | string | yes | The address of the applicant. The format is '{house_number} {street_name} {street_type} {direction}'. street_type and direction are optional. [REQUIRED] | | `applicant_co.zip` | string | yes | The ZIP code of the applicant. [REQUIRED] | | `applicant_co.state` | string | yes | The state of the applicant. [REQUIRED] | | `applicant_co.city` | string | yes | The city of the applicant. Abbreviation of the city name. E.g 'NY'. [REQUIRED] | | `applicant_co.income` | object | | | | `applicant_co.income.gross_monthly` | integer | yes | Gross monthly income, greater than zero. [REQUIRED] | | `applicant_co.income.other_monthly` | integer | | Other monthly income, greater than zero. | | `applicant_co.income.other_source` | string: `aid_for_dependent_children`, `child_support`, `cola`, `disability`, `fssa`, `housing_allowances`, `military_basic_allowance_for_subsistence`, `municipal_bond_interest`, `non_taxable_sources_of_income`, `other`, … (5 more) | | Source of other income. | Example: ```json { "applicant_main": { "first_name": "JANE", "last_name": "SAMPLE", "phone": "", "email": "", "address": "123 MAIN ST", "city": "ANYTOWN", "state": "TX", "zip": "750010000" }, "applicant_co": { "first_name": "JANE", "last_name": "DOE", "phone": "", "email": "", "address": "123 MAIN ST", "city": "ANYTOWN", "state": "TX", "zip": "750010000" } } ``` ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | Verify Premium processed successfully | ExperianVerifyResponse | | 400 | Bad Request | StandardErrorResponse | | 401 | Unauthorized | StandardErrorResponse | | 403 | Product Not Enabled | StandardErrorResponse | | 429 | Too Many Requests | StandardErrorResponse | | 500 | Internal Server Error | StandardErrorResponse | Top-level 200 fields: `pdf_s3_key`, `errors`. --- # Alt-Fi Inquiry `POST https://api.agilitycredit.net/v3/alt-fi` Docs page: https://apidocs.agilitycredit.com/api/run-alt-fi · Tag: ALT-FI This endpoint allows users to perform an Alternative Financial (ALT-FI) inquiry on an applicant. By submitting the necessary applicant information, users can retrieve Employment information such as Annualized Income, Pay Frequency, Current as of Date and Last Pay Date. Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `Idempotency-Key` | header | yes | A unique, idempotent key. We recommend to use a guid | | `Agc-Tenant-Id` | header | yes | The tenant ID provided by Agility Credit. | | `Agc-User-Id` | header | yes | The requestor user ID. | | `Agc-Applicant-Id` | header | | If passed, will assign the report to that applicant. If not, will create a new applicant ID | ## Request body (`application/json`), required Alt-Fi inquiry request payload Schema: `ApplicantInquiry` | Field | Type | Required | Description | | --- | --- | --- | --- | | `applicant_main` | object | yes | | | `applicant_main.first_name` | string | yes | The first name of the applicant. [REQUIRED] | | `applicant_main.middle_name` | string | | The middle name of the applicant. Middle name can be an initial. | | `applicant_main.last_name` | string | yes | The last name of the applicant. [REQUIRED] | | `applicant_main.ssn` | string | | The SSN of the applicant. Required if pull_type is set to 'hard'. | | `applicant_main.dob` | string | | The date of birth of the applicant. Required if pull_type is set to 'hard'. | | `applicant_main.phone` | string | | The phone number of the applicant. | | `applicant_main.email` | string | | The email address of the applicant. | | `applicant_main.address` | string | yes | The address of the applicant. The format is '{house_number} {street_name} {street_type} {direction}'. street_type and direction are optional. [REQUIRED] | | `applicant_main.zip` | string | yes | The ZIP code of the applicant. [REQUIRED] | | `applicant_main.state` | string | yes | The state of the applicant. [REQUIRED] | | `applicant_main.city` | string | yes | The city of the applicant. Abbreviation of the city name. E.g 'NY'. [REQUIRED] | | `applicant_main.income` | object | | | | `applicant_main.income.gross_monthly` | integer | yes | Gross monthly income, greater than zero. [REQUIRED] | | `applicant_main.income.other_monthly` | integer | | Other monthly income, greater than zero. | | `applicant_main.income.other_source` | string: `aid_for_dependent_children`, `child_support`, `cola`, `disability`, `fssa`, `housing_allowances`, `military_basic_allowance_for_subsistence`, `municipal_bond_interest`, `non_taxable_sources_of_income`, `other`, … (5 more) | | Source of other income. | | `applicant_co` | object | | | | `applicant_co.first_name` | string | yes | The first name of the applicant. [REQUIRED] | | `applicant_co.middle_name` | string | | The middle name of the applicant. Middle name can be an initial. | | `applicant_co.last_name` | string | yes | The last name of the applicant. [REQUIRED] | | `applicant_co.ssn` | string | | The SSN of the applicant. Required if pull_type is set to 'hard'. | | `applicant_co.dob` | string | | The date of birth of the applicant. Required if pull_type is set to 'hard'. | | `applicant_co.phone` | string | | The phone number of the applicant. | | `applicant_co.email` | string | | The email address of the applicant. | | `applicant_co.address` | string | yes | The address of the applicant. The format is '{house_number} {street_name} {street_type} {direction}'. street_type and direction are optional. [REQUIRED] | | `applicant_co.zip` | string | yes | The ZIP code of the applicant. [REQUIRED] | | `applicant_co.state` | string | yes | The state of the applicant. [REQUIRED] | | `applicant_co.city` | string | yes | The city of the applicant. Abbreviation of the city name. E.g 'NY'. [REQUIRED] | | `applicant_co.income` | object | | | | `applicant_co.income.gross_monthly` | integer | yes | Gross monthly income, greater than zero. [REQUIRED] | | `applicant_co.income.other_monthly` | integer | | Other monthly income, greater than zero. | | `applicant_co.income.other_source` | string: `aid_for_dependent_children`, `child_support`, `cola`, `disability`, `fssa`, `housing_allowances`, `military_basic_allowance_for_subsistence`, `municipal_bond_interest`, `non_taxable_sources_of_income`, `other`, … (5 more) | | Source of other income. | Example: ```json { "applicant_main": { "first_name": "JANE", "last_name": "SAMPLE", "phone": "", "email": "", "address": "123 MAIN ST", "city": "ANYTOWN", "state": "TX", "zip": "750010000" }, "applicant_co": { "first_name": "JANE", "last_name": "DOE", "phone": "", "email": "", "address": "123 MAIN ST", "city": "ANYTOWN", "state": "TX", "zip": "750010000" } } ``` ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | Alt-Fi inquiry processed successfully | one of: AltFiResponse, InquiryResponseWithBureauError, ComplianceResponse | | 400 | Bad Request | BadRequestResponse | | 401 | Unauthorized | UnauthorizedResponse | | 429 | Too Many Requests | TooManyRequestsResponse | | 500 | Internal Server Error | AgilityServerErrorResponse | The 200 body is one of: `AltFiResponse`, `InquiryResponseWithBureauError`, `ComplianceResponse`. ### Error bodies 400: ```json { "status": "Bad Request - Invalid Parameters", "error": "Failed schema validation. Error: data.pull_type must be one of ['prequal', 'hard'], Path: ['data', 'pull_type'], Data: prequalification" } ``` 401: ```json { "message": "Unauthorized" } ``` 429: ```json { "error": "Too Many Requests" } ``` 500: ```json { "message": "Agility Server Error" } ``` --- # Alt-Fi Income `POST https://api.agilitycredit.net/v3/alt-fi/income` Docs page: https://apidocs.agilitycredit.com/api/run-alt-fi-income · Tag: ALT-FI This endpoint allows users to perform an Alternative Financial (ALT-FI) inquiry on an applicant. By submitting the necessary applicant information, users can retrieve the Annualized Income. Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `Idempotency-Key` | header | yes | A unique, idempotent key. We recommend to use a guid | | `Agc-Tenant-Id` | header | yes | The tenant ID provided by Agility Credit. | | `Agc-User-Id` | header | yes | The requestor user ID. | | `Agc-Applicant-Id` | header | | If passed, will assign the report to that applicant. If not, will create a new applicant ID | ## Request body (`application/json`), required Alt-Fi inquiry request payload Schema: `ApplicantInquiry` | Field | Type | Required | Description | | --- | --- | --- | --- | | `applicant_main` | object | yes | | | `applicant_main.first_name` | string | yes | The first name of the applicant. [REQUIRED] | | `applicant_main.middle_name` | string | | The middle name of the applicant. Middle name can be an initial. | | `applicant_main.last_name` | string | yes | The last name of the applicant. [REQUIRED] | | `applicant_main.ssn` | string | | The SSN of the applicant. Required if pull_type is set to 'hard'. | | `applicant_main.dob` | string | | The date of birth of the applicant. Required if pull_type is set to 'hard'. | | `applicant_main.phone` | string | | The phone number of the applicant. | | `applicant_main.email` | string | | The email address of the applicant. | | `applicant_main.address` | string | yes | The address of the applicant. The format is '{house_number} {street_name} {street_type} {direction}'. street_type and direction are optional. [REQUIRED] | | `applicant_main.zip` | string | yes | The ZIP code of the applicant. [REQUIRED] | | `applicant_main.state` | string | yes | The state of the applicant. [REQUIRED] | | `applicant_main.city` | string | yes | The city of the applicant. Abbreviation of the city name. E.g 'NY'. [REQUIRED] | | `applicant_main.income` | object | | | | `applicant_main.income.gross_monthly` | integer | yes | Gross monthly income, greater than zero. [REQUIRED] | | `applicant_main.income.other_monthly` | integer | | Other monthly income, greater than zero. | | `applicant_main.income.other_source` | string: `aid_for_dependent_children`, `child_support`, `cola`, `disability`, `fssa`, `housing_allowances`, `military_basic_allowance_for_subsistence`, `municipal_bond_interest`, `non_taxable_sources_of_income`, `other`, … (5 more) | | Source of other income. | | `applicant_co` | object | | | | `applicant_co.first_name` | string | yes | The first name of the applicant. [REQUIRED] | | `applicant_co.middle_name` | string | | The middle name of the applicant. Middle name can be an initial. | | `applicant_co.last_name` | string | yes | The last name of the applicant. [REQUIRED] | | `applicant_co.ssn` | string | | The SSN of the applicant. Required if pull_type is set to 'hard'. | | `applicant_co.dob` | string | | The date of birth of the applicant. Required if pull_type is set to 'hard'. | | `applicant_co.phone` | string | | The phone number of the applicant. | | `applicant_co.email` | string | | The email address of the applicant. | | `applicant_co.address` | string | yes | The address of the applicant. The format is '{house_number} {street_name} {street_type} {direction}'. street_type and direction are optional. [REQUIRED] | | `applicant_co.zip` | string | yes | The ZIP code of the applicant. [REQUIRED] | | `applicant_co.state` | string | yes | The state of the applicant. [REQUIRED] | | `applicant_co.city` | string | yes | The city of the applicant. Abbreviation of the city name. E.g 'NY'. [REQUIRED] | | `applicant_co.income` | object | | | | `applicant_co.income.gross_monthly` | integer | yes | Gross monthly income, greater than zero. [REQUIRED] | | `applicant_co.income.other_monthly` | integer | | Other monthly income, greater than zero. | | `applicant_co.income.other_source` | string: `aid_for_dependent_children`, `child_support`, `cola`, `disability`, `fssa`, `housing_allowances`, `military_basic_allowance_for_subsistence`, `municipal_bond_interest`, `non_taxable_sources_of_income`, `other`, … (5 more) | | Source of other income. | Example: ```json { "applicant_main": { "first_name": "JANE", "last_name": "SAMPLE", "phone": "", "email": "", "address": "123 MAIN ST", "city": "ANYTOWN", "state": "TX", "zip": "750010000" }, "applicant_co": { "first_name": "JANE", "last_name": "DOE", "phone": "", "email": "", "address": "123 MAIN ST", "city": "ANYTOWN", "state": "TX", "zip": "750010000" } } ``` ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | Alt-Fi inquiry processed successfully | one of: AltFiIncomeResponse, InquiryResponseWithBureauError, ComplianceResponse | | 400 | Bad Request | BadRequestResponse | | 401 | Unauthorized | UnauthorizedResponse | | 429 | Too Many Requests | TooManyRequestsResponse | | 500 | Internal Server Error | AgilityServerErrorResponse | The 200 body is one of: `AltFiIncomeResponse`, `InquiryResponseWithBureauError`, `ComplianceResponse`. ### Error bodies 400: ```json { "status": "Bad Request - Invalid Parameters", "error": "Failed schema validation. Error: data.pull_type must be one of ['prequal', 'hard'], Path: ['data', 'pull_type'], Data: prequalification" } ``` 401: ```json { "message": "Unauthorized" } ``` 429: ```json { "error": "Too Many Requests" } ``` 500: ```json { "message": "Agility Server Error" } ``` --- # Plate To Vin `POST https://api.agilitycredit.net/v3/p2v` Docs page: https://apidocs.agilitycredit.com/api/run-p-2-v · Tag: Plate To Vin Initiates a plate to vin inquiry to retrieve vehicle information based on the provided license plate details. This endpoint processes the request and returns relevant vehicle data or errors if applicable. Use this operation to obtain vehicle information for various applications. Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `Idempotency-Key` | header | yes | A unique, idempotent key. We recommend to use a guid | | `Agc-Tenant-Id` | header | yes | The tenant ID provided by Agility Credit. | | `Agc-User-Id` | header | yes | The requestor user ID. | ## Request body (`application/json`), required Inquiry request payload Schema: `PlateToVin` | Field | Type | Required | Description | | --- | --- | --- | --- | | `plate_number` | string | yes | The plate number of the vehicle. | | `plate_state` | string | yes | The state in which the vehicle is registered. | ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | Plate to Vin processed successfully | PlateToVinResponse | | 400 | Bad Request | BadRequestResponsePlateToVin | | 401 | Unauthorized | UnauthorizedResponse | | 429 | Too Many Requests | TooManyRequestsResponse | | 500 | Internal Server Error | AgilityServerErrorResponse | Top-level 200 fields: `Header`, `Body`. ### Error bodies 400: ```json { "status": "Bad Request - Validation Failed", "error": "Failed schema validation. Error: data must contain ['plate_number'] properties, Path: ['data'], Data: {'plate_state': 'CA', 'stage': 'v3'}" } ``` 401: ```json { "message": "Unauthorized" } ``` 429: ```json { "error": "Too Many Requests" } ``` 500: ```json { "message": "Agility Server Error" } ``` --- # List Tenants `GET https://api.agilitycredit.net/v3/tenants` Docs page: https://apidocs.agilitycredit.com/api/list-tenants · Tag: Administration The "List associated Tenants" section allows users to retrieve a list of tenants associated with their account in the Agility Credit API. By accessing this section, users can easily view and manage tenant information for streamlined operations within the platform. The response body is a JSON array. Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `Agc-Tenant-Id` | header | yes | The tenant ID provided by Agility Credit. | | `Agc-User-Id` | header | yes | The requestor user ID. | ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | Tenants | TenantsResponse | | 400 | Bad Request | BadRequestResponse | | 401 | Unauthorized | UnauthorizedResponse | | 429 | Too Many Requests | TooManyRequestsResponse | | 500 | Internal Server Error | AgilityServerErrorResponse | ### Error bodies 400: ```json { "status": "Bad Request - Invalid Parameters", "error": "Failed schema validation. Error: data.pull_type must be one of ['prequal', 'hard'], Path: ['data', 'pull_type'], Data: prequalification" } ``` 401: ```json { "message": "Unauthorized" } ``` 429: ```json { "error": "Too Many Requests" } ``` 500: ```json { "message": "Agility Server Error" } ``` --- # Create a Tenant `POST https://api.agilitycredit.net/v3/tenants` Docs page: https://apidocs.agilitycredit.com/api/create-tenant · Tag: Administration Creates a customer tenant under your account. Only integrator accounts can create tenants. Your account becomes the parent and integrator of the new tenant, so your API key can act on it by sending the returned `tenant_id` as `Agc-Tenant-Id`. Only the account details below are accepted; every other configuration setting starts at its default and is managed from the Agility Portal. Optionally, send `api_user` to also create a user with the `api` role, to be sent as `Agc-User-Id` when acting on the new tenant. Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `Agc-Tenant-Id` | header | yes | The tenant ID provided by Agility Credit. | | `Agc-User-Id` | header | yes | The requestor user ID. | | `Idempotency-Key` | header | yes | A unique, idempotent key. We recommend to use a guid | ## Request body (`application/json`), required Schema: `CreateTenant` | Field | Type | Required | Description | | --- | --- | --- | --- | | `account_name` | string | yes | | | `legal_name` | string | yes | | | `address_billing` | object | yes | | | `address_billing.street` | string | yes | | | `address_billing.city` | string | yes | | | `address_billing.state_code` | string | yes | Two-letter uppercase state code. | | `address_billing.postal_code` | string | yes | | | `address_billing.country_code` | string | yes | Two-letter uppercase ISO country code. | | `address_physical` | object | yes | | | `address_physical.street` | string | yes | | | `address_physical.city` | string | yes | | | `address_physical.state_code` | string | yes | Two-letter uppercase state code. | | `address_physical.postal_code` | string | yes | | | `address_physical.country_code` | string | yes | Two-letter uppercase ISO country code. | | `phone_number` | string | yes | | | `website` | string | yes | | | `tax_id` | string | yes | | | `company_established_date` | string | | Date in `YYYY-MM-DD` format. | | `timezone` | string | | IANA timezone name. Defaults to `America/Los_Angeles`. | | `subdomain` | string | | Lowercase letters, digits and hyphens (1 to 63 characters, not starting or ending with a hyphen). Must be unique across Agility Credit. Defaults to the generated tenant ID. | | `market` | string: `automotive`, `non-automotive`, `mortgage` | yes | | | `enduser_type` | string | | | | `nature_of_business` | string: `auto_dealer`, `vehicle_financing`, `auto_refinance`, `equipment_financing`, `medical_financing`, `home_improvement_financing`, `buy_now_pay_later`, `unsecured`, `personal_loans`, `tax_relief`, … (6 more) | yes | | | `primary_title` | string | | | | `inspection_email` | string | | | | `inspection_full_name` | string | | | | `inspection_phone` | string | | | | `inspection_title` | string | | | | `product_addons` | object | | Driver license verification is the only product addon that can be changed through the API. `driver_license_verify` must be enabled (in the same request or already on the tenant) to enable `driver_license_verify_doc_liveness` or `driver_lic… | | `product_addons.driver_license_verify` | boolean | | | | `product_addons.driver_license_verify_doc_liveness` | boolean | | | | `product_addons.driver_license_verify_selfie` | boolean | | | | `ip_whitelist` | array of object | | | | `ip_whitelist[].ip` | string | yes | | | `ip_whitelist[].description` | string | yes | | | `api_user` | object | | Optional. Creates a user with the `api` role in the new tenant, whose ID is returned as `api_user_id` and is sent as `Agc-User-Id` when acting on the new tenant. This user has no Portal login. | | `api_user.first_name` | string | yes | | | `api_user.last_name` | string | yes | | | `api_user.email` | string (email) | yes | | | `api_user.phone_number` | string | | | Example: ```json { "account_name": "Acme Motors", "legal_name": "Acme Motors LLC", "address_billing": { "street": "123 Main St", "city": "Austin", "state_code": "TX", "postal_code": "78701", "country_code": "US" }, "address_physical": { "street": "123 Main St", "city": "Austin", "state_code": "TX", "postal_code": "78701", "country_code": "US" }, "phone_number": "5125550100", "website": "https://www.acmemotors.com", "tax_id": "12-3456789", "nature_of_business": "auto_dealer", "market": "automotive", "subdomain": "acmemotors", "product_addons": { "driver_license_verify": true }, "api_user": { "first_name": "Jane", "last_name": "Doe", "email": "api@acmemotors.com" } } ``` ## Responses | Status | Description | Body | | --- | --- | --- | | 201 | Tenant created | CreateTenantResponse | | 400 | Bad Request. Returned when the body fails schema validation, or when `driver_license_verify_doc_liveness` or `driver_license_verify_selfie` is enabled without `driver_license_verify`. | BadRequestResponse | | 401 | Unauthorized | UnauthorizedResponse | | 403 | Your account is not an integrator. | TenantErrorResponse | | 409 | The subdomain is already taken, or a request with the same `Idempotency-Key` is still in progress. | TenantErrorResponse | | 429 | Too Many Requests | TooManyRequestsResponse | | 500 | Internal Server Error | AgilityServerErrorResponse | Top-level 201 fields: `tenant_id`, `parent_id`, `subdomain`, `status`, `created_at`, `api_user_id`. ### Error bodies 400: ```json { "status": "Bad Request - Validation Failed", "error": "data.market must be one of ['automotive', 'non-automotive', 'mortgage']" } ``` 401: ```json { "message": "Unauthorized" } ``` 403: ```json { "error": "Only integrator tenants can create tenants" } ``` 409: ```json { "error": "Subdomain already exists" } ``` 429: ```json { "error": "Too Many Requests" } ``` 500: ```json { "message": "Agility Server Error" } ``` --- # Get a Tenant `GET https://api.agilitycredit.net/v3/tenants/{id}` Docs page: https://apidocs.agilitycredit.com/api/get-tenant · Tag: Administration Returns the account details of a tenant. You can get your own tenant or any tenant you are an integrator of, such as the ones you created. Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | ID of the tenant. | | `Agc-Tenant-Id` | header | yes | The tenant ID provided by Agility Credit. | | `Agc-User-Id` | header | yes | The requestor user ID. | ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | Tenant | TenantResponse | | 400 | Bad Request | BadRequestResponse | | 401 | Unauthorized | UnauthorizedResponse | | 404 | The tenant does not exist, or it is neither your own tenant nor one you are an integrator of. | TenantErrorResponse | | 429 | Too Many Requests | TooManyRequestsResponse | | 500 | Internal Server Error | AgilityServerErrorResponse | Top-level 200 fields: `tenant_id`, `account_name`, `legal_name`, `address_billing`, `address_physical`, `phone_number`, `website`, `tax_id`, `company_established_date`, `timezone`, `subdomain`, `market`, `enduser_type`, `nature_of_business`, `primary_title`, `inspection_email`, `inspection_full_name`, `inspection_phone`, `inspection_title`, `ip_whitelist`, `product_addons`, `parent_id`, `status`, `type`, `created_at`, `updated_at`. ### Error bodies 400: ```json { "status": "Bad Request - Invalid Parameters", "error": "Failed schema validation. Error: data.pull_type must be one of ['prequal', 'hard'], Path: ['data', 'pull_type'], Data: prequalification" } ``` 401: ```json { "message": "Unauthorized" } ``` 404: ```json { "error": "Tenant Not Found" } ``` 429: ```json { "error": "Too Many Requests" } ``` 500: ```json { "message": "Agility Server Error" } ``` --- # Update a Tenant `PATCH https://api.agilitycredit.net/v3/tenants/{id}` Docs page: https://apidocs.agilitycredit.com/api/update-tenant · Tag: Administration Updates the account details and driver license verification addons of a tenant. You can update your own tenant or any tenant you are an integrator of. Every other configuration setting is managed from the Agility Portal. Returns the updated tenant. Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | ID of the tenant. | | `Agc-Tenant-Id` | header | yes | The tenant ID provided by Agility Credit. | | `Agc-User-Id` | header | yes | The requestor user ID. | ## Request body (`application/json`), required Schema: `UpdateTenant` | Field | Type | Required | Description | | --- | --- | --- | --- | | `account_name` | string | | | | `legal_name` | string | | | | `address_billing` | object | | | | `address_billing.street` | string | yes | | | `address_billing.city` | string | yes | | | `address_billing.state_code` | string | yes | Two-letter uppercase state code. | | `address_billing.postal_code` | string | yes | | | `address_billing.country_code` | string | yes | Two-letter uppercase ISO country code. | | `address_physical` | object | | | | `address_physical.street` | string | yes | | | `address_physical.city` | string | yes | | | `address_physical.state_code` | string | yes | Two-letter uppercase state code. | | `address_physical.postal_code` | string | yes | | | `address_physical.country_code` | string | yes | Two-letter uppercase ISO country code. | | `phone_number` | string | | | | `website` | string | | | | `tax_id` | string | | | | `company_established_date` | string | | Date in `YYYY-MM-DD` format. | | `timezone` | string | | IANA timezone name. Defaults to `America/Los_Angeles`. | | `subdomain` | string | | Lowercase letters, digits and hyphens (1 to 63 characters, not starting or ending with a hyphen). Must be unique across Agility Credit. Defaults to the generated tenant ID. | | `market` | string: `automotive`, `non-automotive`, `mortgage` | | | | `enduser_type` | string | | | | `nature_of_business` | string: `auto_dealer`, `vehicle_financing`, `auto_refinance`, `equipment_financing`, `medical_financing`, `home_improvement_financing`, `buy_now_pay_later`, `unsecured`, `personal_loans`, `tax_relief`, … (6 more) | | | | `primary_title` | string | | | | `inspection_email` | string | | | | `inspection_full_name` | string | | | | `inspection_phone` | string | | | | `inspection_title` | string | | | | `product_addons` | object | | Driver license verification is the only product addon that can be changed through the API. `driver_license_verify` must be enabled (in the same request or already on the tenant) to enable `driver_license_verify_doc_liveness` or `driver_lic… | | `product_addons.driver_license_verify` | boolean | | | | `product_addons.driver_license_verify_doc_liveness` | boolean | | | | `product_addons.driver_license_verify_selfie` | boolean | | | | `ip_whitelist` | array of object | | | | `ip_whitelist[].ip` | string | yes | | | `ip_whitelist[].description` | string | yes | | Example: ```json { "phone_number": "5125550199", "product_addons": { "driver_license_verify_selfie": true } } ``` ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | Tenant updated | TenantResponse | | 400 | Bad Request. Returned when the body fails schema validation, when no field is sent, or when `driver_license_verify_doc_liveness` or `driver_license_verify_selfie` would be enabled without `driver_license_verify`. | BadRequestResponse | | 401 | Unauthorized | UnauthorizedResponse | | 404 | The tenant does not exist, or it is neither your own tenant nor one you are an integrator of. | TenantErrorResponse | | 409 | The subdomain is already taken. | TenantErrorResponse | | 429 | Too Many Requests | TooManyRequestsResponse | | 500 | Internal Server Error | AgilityServerErrorResponse | Top-level 200 fields: `tenant_id`, `account_name`, `legal_name`, `address_billing`, `address_physical`, `phone_number`, `website`, `tax_id`, `company_established_date`, `timezone`, `subdomain`, `market`, `enduser_type`, `nature_of_business`, `primary_title`, `inspection_email`, `inspection_full_name`, `inspection_phone`, `inspection_title`, `ip_whitelist`, `product_addons`, `parent_id`, `status`, `type`, `created_at`, `updated_at`. ### Error bodies 400: ```json { "error": "driver_license_verify must be enabled to enable driver_license_verify_selfie" } ``` 401: ```json { "message": "Unauthorized" } ``` 404: ```json { "error": "Tenant Not Found" } ``` 409: ```json { "error": "Subdomain already exists" } ``` 429: ```json { "error": "Too Many Requests" } ``` 500: ```json { "message": "Agility Server Error" } ``` --- # SSO & SCIM Overview Agility Credit supports federated sign-on so your users can authenticate against your existing identity provider (IdP) — Okta, Entra ID (Azure AD), or any standards-compliant SAML 2.0 / OIDC provider. You can optionally enable SCIM for automated user provisioning. This section is for tenant administrators configuring the connection between your IdP and Agility Credit. ## What you get - **Single sign-on (SSO)** — Users sign in with your IdP credentials instead of an Agility password. - **Automatic role assignment** — Map IdP groups or attributes to Agility roles, applied on every sign-in. - **SCIM provisioning (optional)** — Pre-provision users and groups from your IdP; disable just-in-time (JIT) creation. - **Activity log** — Every SSO and SCIM event is recorded and visible in the portal. - **Emergency access** — Let a few [SSO-exempt users](https://apidocs.agilitycredit.com/sso/sso-exempt-users) keep signing in with a password if your IdP is unavailable. ## Supported configurations | Protocol | Officially supported IdPs | Other IdPs | | ------------ | ------------------------- | ------------------------------------------------------ | | **SAML 2.0** | Okta, Entra ID | Any SAML 2.0 IdP works but is not officially supported | | **OIDC** | Okta, Entra ID | Any OIDC IdP works but is not officially supported | Use **SAML 2.0** if your IdP team is more familiar with it or if your enterprise security policy mandates SAML. Use **OIDC** for a simpler setup with fewer moving parts (no certificates to rotate). ## How the flow works 1. A user visits the portal and clicks **Login with SSO**. 2. They enter their work email and receive a 6-digit verification code (OTP). 3. After verifying the code, they're redirected to your IdP. 4. Your IdP authenticates the user and returns claims to Agility. 5. Agility resolves the user's roles using your **role mapping** configuration, then issues a portal session. If the user belongs to multiple SSO-enabled tenants, they pick which tenant to sign in to before the redirect. ## SCIM × SSO matrix How sign-on behaves depends on whether SCIM provisioning is enabled for your tenant: | Tenant state | New user signs in via SSO | Existing user signs in via SSO | | ----------------- | ---------------------------------------------------------------- | -------------------------------------------------------------------------------- | | **SCIM disabled** | User is auto-created (JIT); roles derived from your role mapping | Roles re-synced from your role mapping on every sign-in | | **SCIM enabled** | Sign-in **rejected** — user must be provisioned via SCIM first | Roles come from the SCIM-managed user record; SSO role mapping does not override | When SCIM is enabled, your IdP is the source of truth for both users and their roles. SSO is used only for authentication. ## Endpoints you'll need These are the URLs you'll paste into your IdP application. Production values are shown below; sandbox uses the same paths under a different domain. | Use | Production | Sandbox | | ----------------- | -------------------------------------------------- | ------------------------------------------------ | | SAML ACS URL | `https://sso.agilitycredit.net/auth/saml/callback` | `https://sso.agc-sandbox.com/auth/saml/callback` | | SAML SP Entity ID | `https://sso.agilitycredit.net` | `https://sso.agc-sandbox.com` | | OIDC Redirect URI | `https://sso.agilitycredit.net/auth/oidc/callback` | `https://sso.agc-sandbox.com/auth/oidc/callback` | The exact values for your tenant are also displayed in the portal under **Settings → Account → Identity** in the **SP Metadata** panel, with one-click copy buttons. ## Where to configure SSO All SSO and SCIM settings live in the portal under: > **Settings → Account → Identity** The page has three tabs: - **SSO** — Protocol selection, IdP connection details, role mapping, SSO-exempt users - **SCIM** — Enable provisioning, view the SCIM endpoint and bearer token instructions - **Activity** — Recent SSO and SCIM events (last 7 days), with optional debug logging *(Screenshot: Overview 1)* ## Next steps - [Set up Okta](https://apidocs.agilitycredit.com/sso/setup-okta) — Click-through guide for Okta (SAML + OIDC) - [Set up Entra ID](https://apidocs.agilitycredit.com/sso/setup-entra) — Click-through guide for Microsoft Entra ID - [Role mapping](https://apidocs.agilitycredit.com/sso/role-mapping) — How Agility roles are assigned based on IdP claims - [SCIM provisioning](https://apidocs.agilitycredit.com/sso/scim-provisioning) — Automate user lifecycle from your IdP - [SSO-exempt users](https://apidocs.agilitycredit.com/sso/sso-exempt-users) — Emergency password access while SSO is enforced - [Troubleshooting](https://apidocs.agilitycredit.com/sso/troubleshooting) — Common errors and how to read the activity log For embedding Agility into other apps, see [Deep links](https://apidocs.agilitycredit.com/deep-links). Source: https://apidocs.agilitycredit.com/sso/overview --- # Set up Okta This guide walks through connecting Okta as your SSO provider. Pick the protocol that fits your environment — **SAML 2.0** or **OIDC**. You only need to set up one. Before you begin, make sure you have: - Okta administrator access - Agility Credit administrator access for your tenant - The list of Okta groups (or a user attribute) that should map to Agility roles ## Pick a protocol | | SAML 2.0 | OIDC | | -------------------- | ------------------------------------------- | ------------------------------------- | | Setup complexity | Higher (certificates, attribute statements) | Lower (a few URLs) | | Certificate rotation | Required | Not applicable | | Recommended when | Your security policy mandates SAML | You have no specific SAML requirement | --- ## Option A — SAML 2.0 ### Step 1: Open the Identity page in Agility 1. Sign in to the Agility Credit portal as an administrator. 2. Open **Settings** from the main navigation. 3. Select **Account**, then **Identity**. 4. On the **SSO** tab, choose **SAML 2.0** as the protocol. Keep this page open — you'll copy values from the **SP Metadata** panel into Okta, and then come back to paste Okta's values here. ### Step 2: Create the SAML app in Okta 1. Sign in to your Okta admin console. 2. Go to **Applications → Applications**, then click **Create App Integration**. 3. Choose **SAML 2.0** and click **Next**. 4. Set **App name** to `Agility Credit` (or your preferred label) and click **Next**. ### Step 3: Configure SAML settings in Okta From the **SP Metadata** panel in Agility, copy these values into Okta: | Okta field | Value to paste | | -------------------------------------------------- | -------------------------------------------------------------------------------------- | | **Single sign-on URL** | The **ACS URL** from Agility (e.g. `https://sso.agilitycredit.net/auth/saml/callback`) | | **Use this for Recipient URL and Destination URL** | Checked | | **Audience URI (SP Entity ID)** | The **SP Entity ID** from Agility (e.g. `https://sso.agilitycredit.net`) | | **Name ID format** | `EmailAddress` | | **Application username** | `Email` | *(Screenshot: Agility OKTA SAML config)* ### Step 4: Add attribute statements Under **Attribute Statements**, add the following. Agility uses these claims to populate the user record. | Name | Value | | -------------- | -------------------------- | | `given_name` | `user.profile.firstName` | | `family_name` | `user.profile.lastName` | | `phone_number` | `user.profile.mobilePhone` | For `given_name`, `family_name`, and `phone_number`, use Okta's expression syntax (`user.profile.firstName` etc.) rather than the older shorthand. `phone_number` is optional — only add it if you want the user's phone to be synced into Agility on sign-in. Under **Group Attribute Statements** (optional, only needed if you use **From IdP groups** role mapping) — this section uses Okta's legacy group-claim configuration, which is still the supported path for SAML group statements: | Name | Name format | Filter | | -------- | ----------- | ------------------------------------------------------------------------- | | `groups` | Unspecified | Matches regex `.*` (or a narrower filter for the groups you want to send) | *(Screenshot: OKTA SAML 2)* Click **Next**, fill out the feedback form, then **Finish**. ### Step 5: Copy Okta's SAML details back to Agility 1. In the Okta app, open the **Sign On** tab. 2. Find the **Metadata URL** (sometimes labeled "Identity Provider metadata"). Copy it. 3. Back in the Agility **Identity** page: - Paste the URL into **Metadata URL** and click **Import**. Agility will fetch and fill the SSO URL, Entity ID, and signing certificate automatically. - Or, if you prefer to fill manually, copy the **Identity Provider Single Sign-On URL**, **Identity Provider Issuer**, and **X.509 Certificate** from Okta into the corresponding fields. *(Screenshot: AGC SAML 3)* ### Step 6: Assign users in Okta In your Okta app, go to **Assignments** and assign the users (or groups) who should have access to Agility. ### Step 7: Test and enable 1. Back in Agility, click **Test Connection** to verify the configuration. 2. Configure your [role mapping](https://apidocs.agilitycredit.com/sso/role-mapping). 3. Toggle **Enable SSO** at the top of the page. 4. Click **Save**. Users in the assigned Okta groups can now sign in via the **Login with SSO** button on the Agility login page. :::caution Don't use Okta's built-in SSO test Okta's app-level test (the **Sign On → Test** option) and the "tile launch" from a user's Okta dashboard trigger an IdP-initiated flow that Agility doesn't accept — it returns `Missing RelayState`. This is by design: Agility only allows sign-ins that start from the Agility login page (SP-initiated), which is the flow your real users will take. To test, open the Agility login page in a private/incognito window, click **Login with SSO**, and complete the flow from there. ::: --- ## Option B — OIDC ### Step 1: Open the Identity page in Agility 1. Sign in to the Agility Credit portal as an administrator. 2. Open **Settings → Account → Identity**. 3. On the **SSO** tab, choose **OIDC** as the protocol. Keep this page open — the **Redirect URI** panel at the bottom shows the value you'll paste into Okta in Step 3. ### Step 2: Create the OIDC app in Okta 1. Sign in to your Okta admin console. 2. Go to **Applications → Applications**, then click **Create App Integration**. 3. Choose **OIDC - OpenID Connect**. 4. Set **Application type** to **Web Application** and click **Next**. *(Screenshot: OKTA OIDC 1)* ### Step 3: Configure the OIDC app Fill in the Okta form: | Okta field | Value | | -------------------------- | ------------------------------------------------------------------------------------------------ | | **App integration name** | `Agility Credit` | | **Grant type** | Authorization Code | | **Sign-in redirect URIs** | The **OIDC Redirect URI** from Agility (e.g. `https://sso.agilitycredit.net/auth/oidc/callback`) | | **Sign-out redirect URIs** | Leave blank or set to your portal URL | | **Controlled access** | Choose who can access the app | Click **Save**. *(Screenshot: OKTA OIDC 2)* ### Step 4: Copy Okta credentials to Agility From the Okta app's **General** tab, copy: | Okta value | Agility field | | ------------------------------------------------------------------------------------------------------------------------- | ----------------- | | **Okta domain** + `/.well-known/openid-configuration` (e.g. `https://your-org.okta.com/.well-known/openid-configuration`) | **Discovery URL** | | **Client ID** | **Client ID** | | **Client Secret** | **Client Secret** | Click **Verify** next to the Discovery URL — Agility will fetch the document and show the resolved issuer, authorization endpoint, and token endpoint. This catches typos before you save. *(Screenshot: OKTA OIDC 3)* ### Step 5: Assign users and test 1. In Okta, go to **Assignments** and assign users or groups. 2. In Agility, click **Test Connection**. 3. Configure your [role mapping](https://apidocs.agilitycredit.com/sso/role-mapping). 4. Toggle **Enable SSO** and click **Save**. --- ## What Okta needs to send For both SAML and OIDC, Agility expects the following claims: | Claim | Required | Notes | | -------------- | ----------- | ------------------------------------------------------------------------------ | | `email` | Yes | Primary identifier for matching to existing Agility users | | `given_name` | Recommended | Used for the user's display name; empty if omitted | | `family_name` | Recommended | Used for the user's display name; empty if omitted | | `phone_number` | Optional | Synced into the user record on every sign-in if sent | | `groups` | Optional | Required only if you use **From IdP groups** role mapping | | `roles` | Optional | Required only if you use **From IdP attribute** role mapping with name `roles` | For OIDC, Agility requests the scopes `openid email profile phone`. Okta returns `phone_number` automatically under the `phone` scope — no extra configuration needed on the Okta side. ## Next steps - [Role mapping](https://apidocs.agilitycredit.com/sso/role-mapping) — Map Okta groups or attributes to Agility roles - [SCIM provisioning](https://apidocs.agilitycredit.com/sso/scim-provisioning) — Auto-provision users from Okta via SCIM - [Troubleshooting](https://apidocs.agilitycredit.com/sso/troubleshooting) — If sign-in fails after configuration Source: https://apidocs.agilitycredit.com/sso/setup-okta --- # Set up Microsoft Entra ID This guide walks through connecting Microsoft Entra ID (formerly Azure AD) as your SSO provider. Pick the protocol that fits your environment — **SAML 2.0** or **OIDC**. You only need to set up one. Before you begin, make sure you have: - Entra administrator access (Application Administrator or Cloud Application Administrator role) - Agility Credit administrator access for your tenant - The list of Entra groups (or a user attribute) that should map to Agility roles ## Pick a protocol | | SAML 2.0 | OIDC | | -------------------- | ------------------------------------------------- | ------------------------------------- | | Setup complexity | Higher (certificates, claim mapping) | Lower (a few URLs) | | Certificate rotation | Required (Entra rotates every 3 years by default) | Not applicable | | Recommended when | Your security policy mandates SAML | You have no specific SAML requirement | --- ## Option A — SAML 2.0 ### Step 1: Open the Identity page in Agility 1. Sign in to the Agility Credit portal as an administrator. 2. Open **Settings → Account → Identity**. 3. On the **SSO** tab, choose **SAML 2.0** as the protocol. Keep this page open so you can copy values between the two consoles. *(Screenshot: Entra SSO)* ### Step 2: Create the Enterprise Application in Entra 1. Sign in to the [Microsoft Entra admin center](https://entra.microsoft.com). 2. Go to **Entra ID → Enterprise applications**. 3. Click **New application**, then **Create your own application**. 4. Name it `Agility Credit`, choose **Integrate any other application you don't find in the gallery (Non-gallery)**, and click **Create**. *(Screenshot: Entra SAML 1)* ### Step 3: Configure single sign-on 1. In the new application, open **Single sign-on** from the left menu. 2. Choose **SAML** as the SSO method. 3. In **Basic SAML Configuration**, click **Edit** and paste: | Entra field | Value to paste | | ---------------------------------------------- | -------------------------------------------------------------------------------------- | | **Identifier (Entity ID)** | The **SP Entity ID** from Agility (e.g. `https://sso.agilitycredit.net`) | | **Reply URL (Assertion Consumer Service URL)** | The **ACS URL** from Agility (e.g. `https://sso.agilitycredit.net/auth/saml/callback`) | | **Sign on URL** | Leave blank | | **Relay State** | Leave blank | | **Logout URL** | Leave blank | Click **Save**. *(Screenshot: Entra SAML 2)* ### Step 4: Configure claims In the **Attributes & Claims** section, click **Edit**. Entra ships with a default set of claims; add or modify these so they match what Agility expects: | Claim name | Source attribute | | -------------- | ---------------------------------------------------------- | | `emailaddress` | `user.mail` (or `user.userprincipalname` if mail is empty) | | `givenname` | `user.givenname` | | `surname` | `user.surname` | | `mobilephone` | `user.telephonenumber` or `user.mobile` _(optional)_ | Set the **Name identifier value** (Name ID) to `user.mail` with format **Email address**. The phone claim is optional — only add it if you want the user's phone number synced into Agility on sign-in. :::tip Recommended — send App Roles instead of groups Entra's default group claim emits **group object IDs** (GUIDs), which are hard to read and impossible to use with SCIM. App Roles let you define stable, readable role values that work consistently across SAML, OIDC, and SCIM. 1. In your **App registration** (not the Enterprise Application — same app, different blade), open **App roles**. 2. Click **Create app role** and add one app role per Agility role you want to grant — for example `Admin`, `User`, `UserWithCredit`, `Accounting`, `API`. The **Value** field is what Entra sends to Agility; pick names you'll reference in your role mapping table. (Agility's `User with Credit` role has spaces, but Entra App Role values can't contain spaces — use something like `UserWithCredit` and map it through.) 3. Back in the **Enterprise Application**, open **Users and groups**, click **Add user/group**, and assign each user (or directory group) to a specific app role. Entra emits the assigned app role in a `roles` claim automatically — no extra claim configuration required. In Agility's [role mapping](https://apidocs.agilitycredit.com/sso/role-mapping), set the **Source** to **From IdP attribute**, **Attribute name** to `roles`, and map each Entra app role value to an Agility role — e.g. `Admin → Admin`, `UserWithCredit → User with Credit`, `API → API`. ::: *(Screenshot: Entra SAML 3)* :::note Alternative: send group display names If you prefer to drive role assignment from directory group membership instead of app roles, click **Add a group claim** in **Attributes & Claims** and configure: - **Which groups should be returned?** — **Groups assigned to the application** (recommended) or **Security groups**. - **Source attribute** — **Cloud-only group display names** for cloud-only Entra groups, or **sAMAccountName** for on-prem AD-synced groups. Avoid **Group ID** — it sends GUIDs that don't work with SCIM and are hard to read. - **Customize the name of the group claim** — set **Name** to `groups`. Then in Agility, use **From IdP groups** role mapping. Note: display names are mutable — if a group is renamed in Entra, your role mapping silently breaks. App Roles avoid that risk. ::: ### Step 5: Set the signing option and copy URLs In the **SAML Certificates** section, click the **edit (pencil)** icon at the top right of the section. - **Signing Option:** change from the default **Sign SAML response** to **Sign SAML response and assertion**. - **Signing Algorithm:** leave at **SHA-256**. - **Save**. *(Screenshot: Entra SAML 5)* :::caution Required — sign both the response and the assertion By default Entra signs only the outer SAML response, but Agility requires the assertion inside to be signed as well. If you skip this step, sign-in fails with `SAML validation failed: Invalid document signature`. This setting also follows Microsoft's own security guidance for SAML SSO. ::: Then, copy the **Certificate (Base64)** content from the same section. In the **Set up Agility Credit** section below, copy: - **App Federation Metadata URL** (recommended — one URL, auto-updates on rotation), or - **Login URL** and **Microsoft Entra Identifier** if you prefer to paste each value. ### Step 6: Paste Entra's values into Agility In the Agility **Identity** page: - Paste the **App Federation Metadata URL** into the **Metadata URL** field and click **Import**. Agility will fetch the SSO URL, Entity ID, and signing certificate automatically. - Or, if you prefer manual entry, paste the **Login URL** into **SSO URL**, the **Microsoft Entra Identifier** into **Entity ID / Issuer**, and the Base64 certificate into **Signing Certificates**. *(Screenshot: Entra SAML 4)* :::tip Certificate rotation Entra rotates SAML certificates periodically. You can paste both the current and the next certificate into Agility — both will be accepted at the same time. Add a second certificate using the **+** icon next to the certificate list before Entra activates the new one to avoid downtime. ::: ### Step 7: Assign users in Entra In your Entra app, open **Users and groups** and assign the users (or groups) who should have access to Agility. ### Step 8: Test and enable 1. In Agility, click **Test Connection** to verify the configuration. 2. Configure your [role mapping](https://apidocs.agilitycredit.com/sso/role-mapping). 3. Toggle **Enable SSO** and click **Save**. :::caution Don't use Entra's "Test single sign-on" button Entra's built-in test triggers an IdP-initiated flow that Agility doesn't accept — it returns `Missing RelayState`. This is by design: Agility only allows sign-ins that start from the Agility login page (SP-initiated), which is the flow your real users will take. To test, open the Agility login page in a private/incognito window, click **Login with SSO**, and complete the flow from there. ::: --- ## Option B — OIDC ### Step 1: Open the Identity page in Agility 1. Sign in to the Agility Credit portal as an administrator. 2. Open **Settings → Account → Identity**. 3. On the **SSO** tab, choose **OIDC** as the protocol. ### Step 2: Register an app in Entra 1. Sign in to the [Microsoft Entra admin center](https://entra.microsoft.com). 2. Go to **Identity → Applications → App registrations** and click **New registration**. 3. Fill out: | Field | Value | | --------------------------- | -------------------------------------------------------------------------------------------------------------------- | | **Name** | `Agility Credit` | | **Supported account types** | Accounts in this organizational directory only (single tenant) | | **Redirect URI** | Platform: **Web** — the **OIDC Redirect URI** from Agility (e.g. `https://sso.agilitycredit.net/auth/oidc/callback`) | Click **Register**. *(Screenshot: Entra OIDC 1)* ### Step 3: Create a client secret 1. In the registered app, open **Certificates & secrets**. 2. Under **Client secrets**, click **New client secret**. 3. Give it a description and an expiry, then click **Add**. 4. **Copy the secret value now** — Entra only shows it once. *(Screenshot: Entra OIDC 3)* ### Step 4: Copy values to Agility From the Entra app's **Overview** page: | Entra value | Agility field | | ---------------------------------------------- | ----------------- | | Discovery URL (see below for where to find it) | **Discovery URL** | | Application (client) ID (see below) | **Client ID** | | The client secret from Step 3 | **Client Secret** | :::info Finding the Discovery URL Entra doesn't display the OIDC discovery URL as a single labeled field — you either copy it from the Endpoints panel or assemble it from your tenant ID. - **Recommended — copy from the Endpoints panel.** On the app's **Overview** page, click the **Endpoints** button at the top. In the dialog, copy the **OpenID Connect metadata document** value. It's a complete URL of the form `https://login.microsoftonline.com//v2.0/.well-known/openid-configuration` — paste that directly into Agility's **Discovery URL** field. - **Assemble manually.** Copy the **Directory (tenant) ID** GUID from the app's Overview page (or from **Entra ID → Overview**), then build: ``` https://login.microsoftonline.com//v2.0/.well-known/openid-configuration ``` Make sure to use the `v2.0/` segment — Entra also exposes a v1.0 endpoint, but Agility expects the v2.0 token format. ::: :::info Finding the Client ID On the same **Overview** page, find the **Essentials** panel near the top. The **Application (client) ID** is the GUID listed there (different from the Directory/tenant ID — Entra shows both). Copy it into Agility's **Client ID** field. ::: Click **Verify** next to the Discovery URL in Agility to confirm the document loads and shows your tenant's endpoints. *(Screenshot: Entra OIDC 4)* ### Step 5: Grant API permissions By default, a new Entra app registration only has the `User.Read` permission. You need to add the OIDC scopes and grant admin consent — otherwise users see a consent prompt on first sign-in, and sign-in fails entirely in tenants that disable user consent. 1. In the App registration, open **API permissions** from the left menu. 2. Click **Add a permission**. 3. Choose **Microsoft Graph**. 4. Choose **Delegated permissions**. 5. In the search box, find and check each of the following: - `openid` - `email` - `profile` 6. Click **Add permissions**. 7. Back on the **API permissions** page, click **Grant admin consent for \** at the top of the permissions table. Confirm in the dialog. After granting consent, the **Status** column should show a green check next to each permission. :::caution Required for production Skipping admin consent makes sign-in fail in enterprise tenants that disable user-level consent (a common security policy). It also forces a per-user consent prompt on first login. Add the permissions and grant admin consent before going live. ::: Leave `User.Read` in place — it's harmless and Entra adds it by default. *(Screenshot: Entra OIDC 2)* ### Step 6: (Optional) Send the user's phone number Entra does not return `phone_number` from the standard OIDC scopes. If you want phone numbers synced into Agility on sign-in, add an optional claim: 1. In the App registration, open **Token configuration**. 2. Click **Add optional claim**. 3. Token type: **ID**. 4. Select **phone_number** (or **phone** if `phone_number` isn't listed). 5. Click **Add**, then confirm the prompt to enable Microsoft Graph permissions if asked. This step is optional — skip it if you don't need phone numbers in Agility. ### Step 7: (Optional) Send role information :::tip Recommended — define App Roles App Roles let you send stable, readable role values that work consistently across SAML, OIDC, and SCIM. The setup is the same as for SAML: 1. In **App registration → App roles**, click **Create app role** and add one app role per Agility role you want to grant (e.g. `Admin`, `User`, `UserWithCredit`, `Accounting`, `API`). The **Value** field is what Entra emits in the token. 2. In the **Enterprise Application → Users and groups**, assign each user (or directory group) to a specific app role. Entra emits the assigned app role in a `roles` claim in the ID token automatically — no extra Token configuration required. In Agility's [role mapping](https://apidocs.agilitycredit.com/sso/role-mapping), set **Source** to **From IdP attribute**, **Attribute name** to `roles`, and map each app role value to an Agility role. ::: :::note Alternative: send group claims If you prefer to drive role assignment from directory group membership instead of app roles: 1. In the Entra app, open **Token configuration**. 2. Click **Add groups claim**. 3. Pick **Groups assigned to the application** (recommended) or **Security groups**. 4. Under **ID**, select **Cloud-only group display names** for cloud-only Entra groups, or **sAMAccountName** for on-prem AD-synced groups. Avoid **Group ID** — it sends GUIDs that don't work with SCIM. 5. Save. Use **From IdP groups** role mapping in Agility. Note: display names are mutable — if a group is renamed in Entra, the mapping silently breaks. App Roles avoid that risk. ::: ### Step 8: Test and enable 1. In Agility, click **Test Connection**. 2. Configure your [role mapping](https://apidocs.agilitycredit.com/sso/role-mapping). 3. Toggle **Enable SSO** and click **Save**. --- ## What Entra needs to send For both SAML and OIDC, Agility expects: | Claim | Required | Notes | | -------------- | ----------- | ----------------------------------------------------------------------------------------------------- | | `email` | Yes | Primary identifier for matching to existing Agility users | | `given_name` | Recommended | Used for the user's display name; empty if omitted | | `family_name` | Recommended | Used for the user's display name; empty if omitted | | `phone_number` | Optional | Synced into the user record on every sign-in if sent. Requires the Token-configuration step for OIDC. | | `roles` | Optional | Required for App-Roles-based role mapping (see Step 4 for SAML, Step 7 for OIDC) | | `groups` | Optional | Required only if you use **From IdP groups** role mapping | For OIDC, Agility requests the scopes `openid email profile phone`. Entra ignores the `phone` scope — to ship phone numbers via OIDC, configure the optional claim in **Token configuration** (see the OIDC Step 6 above). ## Next steps - [Role mapping](https://apidocs.agilitycredit.com/sso/role-mapping) — Map Entra groups or attributes to Agility roles - [SCIM provisioning](https://apidocs.agilitycredit.com/sso/scim-provisioning) — Auto-provision users from Entra via SCIM - [Troubleshooting](https://apidocs.agilitycredit.com/sso/troubleshooting) — If sign-in fails after configuration Source: https://apidocs.agilitycredit.com/sso/setup-entra --- # Role mapping Role mapping controls how Agility roles are assigned to users signing in via SSO. It runs on **every** SSO sign-in, so a user's roles always reflect their current state in your IdP (when SCIM is disabled). Configure role mapping under **Settings → Account → Identity → SSO** in the **Role mapping** section. *(Screenshot: Role Mapping 1)* ## The three sources Pick the source that fits how your IdP is structured. ### Portal-managed > Roles are set manually in the Agility portal. Token claims are ignored. Use this when you want SSO only for authentication, and Agility administrators continue to assign roles by hand in the portal's user management page. - **Pros:** Simplest setup. No IdP changes needed. - **Cons:** No automation — you'll need to add/remove roles manually as people change teams. ### From IdP groups > Roles derived from a `groups` claim in the SSO token, mapped via the table below. Use this when your IdP sends group memberships and you want group membership to drive Agility roles. You configure a mapping table: | IdP group | Agility role | | ---------------------- | ---------------- | | `agility-admins` | Admin | | `agility-users` | User | | `agility-credit-users` | User with Credit | | `agility-accounting` | Accounting | | `agility-api` | API | A user can be assigned **multiple roles** if they're a member of multiple mapped groups. If they're in none of the mapped groups, they receive no roles (which typically means they cannot use the portal). :::tip Group naming Group names must match exactly — they are not case-insensitive or pattern-matched. Make sure the value you enter matches what your IdP actually sends. If you're unsure, enable **Log successful events (debug)** on the **Activity** tab and inspect the claim values in the event log after a test sign-in. ::: ### From IdP attribute > Roles derived from a single named claim in the SSO token (one role per value). Use this when your IdP doesn't expose group memberships, or when you'd rather drive role assignment from a custom user attribute (e.g. `department`, `jobTitle`, a custom `agcRole` claim). You configure: - **Attribute name** — The name of the claim Agility should read (e.g. `department`). - A mapping table from attribute values to Agility roles. | Attribute value | Agility role | | --------------- | ------------ | | `Finance` | Accounting | | `Compliance` | Admin | If the claim is multi-valued, each value is matched independently and matching roles are combined. :::tip Recommended for Entra ID — use App Roles If you're on Entra, this is the source you want, with **Attribute name** set to `roles`. Define App Roles in your Entra app registration (e.g. `Admin`, `User`, `UserWithCredit`, `Accounting`, `API`), assign users to them, and Entra emits the role values in a `roles` claim across SAML, OIDC, and SCIM with the same setup. This avoids the GUID-vs-name problem of group claims and is immune to group renames. See [Set up Entra ID](https://apidocs.agilitycredit.com/sso/setup-entra) for the click-through. ::: ## How matching works - Matching is **exact, case-sensitive**. - Multiple matches assign multiple roles. - No match assigns no roles. - There is no default fallback role — if you want all SSO users to get at least one role, include a catch-all group everyone belongs to. ## When role mapping does not apply If your tenant has **SCIM provisioning** enabled, role mapping behaves differently: - For **new users**, SSO sign-in is rejected — users must be created via SCIM first. - For **existing users**, roles come from the user's record (managed by SCIM). SSO role mapping is not re-applied at sign-in. In short: when SCIM is enabled, SCIM owns user lifecycle and roles. SSO is used only to authenticate. See [SCIM provisioning](https://apidocs.agilitycredit.com/sso/scim-provisioning) for details. ## Recommended setup The right source depends on your IdP: **Entra ID — use App Roles (`From IdP attribute`, name `roles`):** 1. Define App Roles in your Entra app registration matching the Agility roles you want to grant (e.g. `Admin`, `User`, `UserWithCredit`, `Accounting`, `API`) and assign users to them. 2. In Agility, set **Source** to **From IdP attribute** and **Attribute name** to `roles`. 3. Map each app role value to an Agility role. Works identically across SAML, OIDC, and SCIM. See [Set up Entra ID](https://apidocs.agilitycredit.com/sso/setup-entra). **Okta — use IdP groups (`From IdP groups`):** 1. Create groups in Okta for each Agility role you want to grant — for example `agility-admins`, `agility-users`, `agility-credit-users`, `agility-accounting`, `agility-api` — and assign users to them. 2. In Agility, set **Source** to **From IdP groups**. 3. Map each group name to the corresponding Agility role. See [Set up Okta](https://apidocs.agilitycredit.com/sso/setup-okta). **Fallback — Portal-managed:** Use **Portal-managed** only if your IdP team can't (or won't) send group or role claims. Roles will then be assigned by Agility administrators manually. ## Verifying your mapping 1. Save the configuration. 2. On the **Activity** tab, toggle **Log successful events (debug)** on. 3. Have a test user sign in via SSO. 4. Open the resulting `sso_login_success` event (or `sso_user_attributes_updated` / `sso_user_created` if anything changed) in the activity log and expand the detail row to see the exact claim values Agility received and which roles were assigned. 5. Adjust the mapping table if needed. 6. Turn the debug toggle off once you've confirmed everything works — successful sign-ins generate a lot of log volume. ## Next steps - [SCIM provisioning](https://apidocs.agilitycredit.com/sso/scim-provisioning) — Enable automated user provisioning from your IdP - [Troubleshooting](https://apidocs.agilitycredit.com/sso/troubleshooting) — If users sign in with no roles or the wrong roles Source: https://apidocs.agilitycredit.com/sso/role-mapping --- # SCIM provisioning SCIM (System for Cross-domain Identity Management) lets your IdP automatically create, update, and deactivate Agility Credit users based on assignments and group memberships in your IdP. When SCIM is enabled, your IdP becomes the source of truth for user lifecycle and role assignment. SCIM is configured under **Settings → Account → Identity → SCIM**. *(Screenshot: SCIM 1)* ## When to use SCIM Use SCIM if you want: - **Automatic provisioning** — Users get an Agility account the moment they're assigned to the Agility app in your IdP. - **Automatic deprovisioning** — Removing a user from the app or disabling them in your IdP locks them out of Agility. - **Group-driven roles** — Role changes in your IdP flow through to Agility on the next sync, not on the next sign-in. If you only need authentication, **don't enable SCIM** — just use SSO with role mapping. SCIM adds operational overhead and requires the user to exist in Agility _before_ their first sign-in. ## SCIM × SSO behavior | Scenario | Without SCIM | With SCIM | | ------------------------------ | ------------------------------------------------- | ------------------------------------------------------------ | | New user signs in via SSO | Auto-created (JIT); roles from your role mapping | Sign-in rejected with `scim_required_user_not_provisioned` | | Existing user signs in via SSO | Roles re-synced from role mapping every sign-in | Roles read from the SCIM-managed record; SSO mapping ignored | | User removed in IdP | Account remains in Agility until manually deleted | Deprovisioned automatically on the next SCIM sync | ## Supported IdPs Agility's SCIM endpoint is SCIM 2.0–compliant and is officially supported with: - **Okta** — using Okta's SCIM 2.0 provisioning - **Microsoft Entra ID** — using Entra's SCIM provisioning Other SCIM 2.0 clients may work — if your IdP sends a valid SCIM 2.0 bearer-authenticated payload, our endpoint will respond — but they are not officially tested or supported. ## Enable SCIM ### Step 1: Create an API key SCIM authenticates using an Agility API key as a bearer token. 1. In the portal, go to **Settings → Account → API Keys**. 2. Click **Create API key**, give it a descriptive name (e.g. `Okta SCIM`), and save. 3. **Copy the full key value** — Agility only displays it once. *(Screenshot: SCIM 2)* :::tip Use a dedicated key Create a separate API key for SCIM rather than reusing one used by other integrations. That way you can rotate it independently if your IdP credentials are compromised. ::: ### Step 2: Enable SCIM in Agility 1. Go to **Settings → Account → Identity** and open the **SCIM** tab. 2. Toggle **Enable SCIM provisioning** on. 3. Note the **SCIM endpoint** URL displayed in the page — it's of the form: ``` https://api.agilitycredit.net/scim/v2/ ``` (UAT uses `https://api.agc-dev.com/scim/v2/`. The exact value for your tenant is shown in the portal with a copy button.) 4. Click **Save**. ### Step 3: Configure your IdP #### Okta 1. In your Okta app, go to **Provisioning → To App**. 2. Click **Configure API Integration**, check **Enable API integration**. 3. Enter: - **Base URL:** the SCIM endpoint from Agility - **Unique identifier field for users:** `userName` - **Authentication Mode:** **HTTP Header** - **Authorization:** paste the API key from Step 1 (Okta uses this verbatim as the `Authorization: Bearer ` header) 4. Click **Test API Credentials**, then **Save**. 5. Under **Provisioning → To App → Provisioning Settings**, enable the actions you need: - **Push New Users** — create users in Agility when assigned in Okta - **Push Profile Updates** — keep user attributes (name, email, phone) in sync - **Push Groups** — sync Okta groups to Agility (required if you use **From IdP groups** role mapping with SCIM) :::caution Pick `userName` as the unique identifier Agility's SCIM endpoint matches users by the SCIM 2.0 standard `userName`. If you pick anything else (e.g. `email` or `id`), Okta can't reliably correlate the Okta user to the Agility record across attribute changes — every update will create a new duplicate user in Agility. ::: *(Screenshot: SCIM 3)* #### Entra ID 1. In your Entra Enterprise Application, go to **Provisioning**. 2. Set **Provisioning Mode** to **Automatic**. 3. Under **Admin Credentials**, enter: - **Tenant URL:** the SCIM endpoint from Agility - **Secret Token:** the API key from Step 1 4. Click **Test Connection**, then **Save**. 5. Under **Mappings**, review and customize the attribute mappings if needed (the defaults work for most setups). 6. Set **Provisioning Status** to **On**. *(Screenshot: SCIM 4)* ##### Recommended: provision roles via App Roles If you're driving role assignment with **App Roles** (see [Set up Entra ID](https://apidocs.agilitycredit.com/sso/setup-entra)), add one extra attribute mapping so Entra pushes role assignments through SCIM: 1. In the Enterprise Application → **Provisioning → Mappings**, open **Provision Microsoft Entra ID Users**. 2. Scroll to **Attribute Mappings** and click **Add New Mapping**. 3. Configure: - **Mapping type:** Expression - **Expression:** `SingleAppRoleAssignment([appRoleAssignments])` - **Target attribute:** `roles[primary eq "true"].value` - **Match objects using this attribute:** No - **Apply this mapping:** Always 4. **Save**. (If users in your tenant need multiple concurrent app roles, use the expression `AppRoleAssignmentsComplex([appRoleAssignments])` with the target attribute `roles`. `SingleAppRoleAssignment` is the common case.) Without this mapping, app role assignments stay in Entra but aren't pushed to Agility — SCIM-provisioned users would arrive without roles. In Agility's role mapping, set **Source** to **From IdP attribute** and **Attribute name** to `roles`. ### Step 4: Assign users and run the initial sync 1. In your IdP, assign the users (and groups) you want provisioned. 2. Trigger an initial sync — both Okta and Entra do this automatically once provisioning is on. 3. In Agility, switch to the **Activity** tab and watch for `scim_provision` events. Successful provisioning events appear with the user's email and an HTTP 201 status. ## What gets synced - **Users** — Email, given name, family name, active state - **Groups** — Membership (used for role mapping if you've configured **From IdP groups**) - **Roles** — Derived from SCIM data according to your role mapping configuration (see below) ## Role mapping with SCIM The same role mapping configuration applies to SCIM-provisioned users — but it runs at **provisioning time**, not at sign-in time. Pick one of: - **Portal-managed** — SCIM does not update roles. Administrators set them in the Agility portal. - **From IdP groups** — Roles derived from SCIM group memberships. Updates to group membership in your IdP flow through on the next sync. - **From IdP attribute** — Roles derived from a configurable SCIM user attribute. See [Role mapping](https://apidocs.agilitycredit.com/sso/role-mapping) for full details. ## Monitoring SCIM activity The **Activity** tab on the Identity page shows recent SCIM operations: - `scim_provision` — User created or updated - `scim_deprovision` — User deactivated - `scim_error` — A SCIM request failed (with details) Toggle **Log successful events (debug)** on to also record `scim_read` events, which include the full request and response payloads. Useful for diagnosing attribute mapping problems; disable it after go-live to keep the log readable. ## SCIM and SSO-exempt users A user provisioned by SCIM can be [SSO-exempt](https://apidocs.agilitycredit.com/sso/sso-exempt-users), and creating or updating them over SCIM keeps the exemption. Deactivating or deleting them over SCIM removes it, and that's audited and emailed to your administrators. Because your IdP can still deactivate or demote such a user, keep emergency access accounts out of the users and groups assigned to the Agility app in your IdP. ## Disabling SCIM If you turn SCIM off: - Existing user records stay in Agility — they aren't deleted. - New SSO users start being JIT-created again on first sign-in. - Roles are managed by role mapping on every SSO sign-in (or stay manual if you chose **Portal-managed**). ## Troubleshooting See the [Troubleshooting](https://apidocs.agilitycredit.com/sso/troubleshooting) page for common SCIM errors and how to read the activity log. Source: https://apidocs.agilitycredit.com/sso/scim-provisioning --- # 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](https://apidocs.agilitycredit.com/sso/scim-provisioning) instead. Agility's SCIM endpoint implements the [SCIM 2.0 protocol](https://datatracker.ietf.org/doc/html/rfc7644) 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/` | | UAT | `https://api.agc-dev.com/scim/v2/` | All requests must include: ``` Authorization: Bearer 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`. :::tip Use a dedicated API key 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](#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 schemas - `GET /Schemas/urn:ietf:params:scim:schemas:core:2.0:User` - `GET /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](#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 `value`s: - `admin` - `user` - `sales` - `sales_with_credit` - `api` - `accounting` 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: ```json "roles": ["admin", "sales"] ``` ```json "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). ```bash curl "https://api.agilitycredit.net/scim/v2//Users?filter=userName%20eq%20%22jane%40acme.com%22" \ -H "Authorization: Bearer " ``` ```json { "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//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` ```bash curl -X POST "https://api.agilitycredit.net/scim/v2//Users" \ -H "Authorization: Bearer " \ -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 missing `userName`/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: ```json { "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): ```json { "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 /Groups` and `DELETE /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`/`204` so strict clients don't break. ### List groups — `GET /Groups` Optional `filter` parameter — only `displayName eq ""` 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. ```json { "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//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 \"\"]"`) works as well as the unpathed `remove` of the entire `members` collection. ```json { "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: ```json { "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 1. Enable SCIM on a test tenant and toggle **Log successful events (debug)** on. With debug on, every successful `GET` is recorded in addition to mutations. 2. Implement against `GET /ServiceProviderConfig` first — it confirms auth, base URL, and tenant ID in one call. 3. Drive a representative end-to-end flow: create a user, PATCH a field, deactivate, reactivate (via a fresh POST), and finally delete. 4. 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. 5. Turn debug off before going live to keep the log readable. See [Troubleshooting](https://apidocs.agilitycredit.com/sso/troubleshooting) for common SCIM errors and how to interpret the activity log. Source: https://apidocs.agilitycredit.com/sso/scim-provisioning/custom-integration --- # Agility Credit SCIM API reference SCIM 2.0 endpoints for provisioning users and groups in Agility Credit. These endpoints implement the [SCIM 2.0 protocol](https://datatracker.ietf.org/doc/html/rfc7644) with the same operation surface that Okta's provisioning client uses. They are intended for IdPs (Okta, Microsoft Entra ID) and custom provisioning clients — see the [SCIM custom integration guide](https://apidocs.agilitycredit.com/sso/scim-provisioning/custom-integration) for the behavioural notes that don't fit a machine-readable spec (PATCH normalization rules, silent-ignore semantics, virtual groups, tenant-configurable role attribute, etc.). Version: 2.0 ## Servers - Production: `https://api.agilitycredit.net/scim/v2/{tenantId}` - UAT: `https://api.agc-dev.com/scim/v2/{tenantId}` ## Authentication HTTP Bearer: send `Authorization: Bearer ` on every request. ## Endpoints - [Get service provider configuration](https://apidocs.agilitycredit.com/sso/scim-api/get-service-provider-config.md): `GET /ServiceProviderConfig` - [List schemas](https://apidocs.agilitycredit.com/sso/scim-api/list-schemas.md): `GET /Schemas` - [Get a schema](https://apidocs.agilitycredit.com/sso/scim-api/get-schema.md): `GET /Schemas/{schemaId}` - [List users](https://apidocs.agilitycredit.com/sso/scim-api/list-users.md): `GET /Users` - [Create user](https://apidocs.agilitycredit.com/sso/scim-api/create-user.md): `POST /Users` - [Get user](https://apidocs.agilitycredit.com/sso/scim-api/get-user.md): `GET /Users/{userId}` - [Replace user](https://apidocs.agilitycredit.com/sso/scim-api/replace-user.md): `PUT /Users/{userId}` - [Update user](https://apidocs.agilitycredit.com/sso/scim-api/patch-user.md): `PATCH /Users/{userId}` - [Deactivate user](https://apidocs.agilitycredit.com/sso/scim-api/delete-user.md): `DELETE /Users/{userId}` - [List groups](https://apidocs.agilitycredit.com/sso/scim-api/list-groups.md): `GET /Groups` - [Create group (idempotent)](https://apidocs.agilitycredit.com/sso/scim-api/create-group.md): `POST /Groups` - [Get group](https://apidocs.agilitycredit.com/sso/scim-api/get-group.md): `GET /Groups/{groupId}` - [Replace group membership](https://apidocs.agilitycredit.com/sso/scim-api/replace-group.md): `PUT /Groups/{groupId}` - [Patch group membership](https://apidocs.agilitycredit.com/sso/scim-api/patch-group.md): `PATCH /Groups/{groupId}` - [Clear group membership](https://apidocs.agilitycredit.com/sso/scim-api/delete-group.md): `DELETE /Groups/{groupId}` OpenAPI spec: https://apidocs.agilitycredit.com/agc-scim-openapi.yaml --- # Get service provider configuration `GET https://api.agilitycredit.net/scim/v2/{tenantId}/ServiceProviderConfig` Docs page: https://apidocs.agilitycredit.com/sso/scim-api/get-service-provider-config · Tag: Discovery Advertises which optional SCIM features Agility supports. Mirror these flags in your client: `patch=true`, `bulk=false`, `filter=true` (`maxResults=200`), `changePassword=false`, `sort=false`, `etag=false`, `groups=true`. Authentication: `Authorization: Bearer `. ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | Service provider configuration | ServiceProviderConfig | | 503 | SCIM provisioning is disabled on the tenant. | Error | ### Error bodies 503: ```json { "schemas": [ "urn:ietf:params:scim:api:messages:2.0:Error" ], "status": "503", "detail": "SCIM provisioning is disabled for this tenant" } ``` --- # List schemas `GET https://api.agilitycredit.net/scim/v2/{tenantId}/Schemas` Docs page: https://apidocs.agilitycredit.com/sso/scim-api/list-schemas · Tag: Discovery Returns the User and Group schemas as a SCIM ListResponse. Authentication: `Authorization: Bearer `. ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | List of schemas | SchemaListResponse | | 503 | SCIM provisioning is disabled on the tenant. | Error | ### Error bodies 503: ```json { "schemas": [ "urn:ietf:params:scim:api:messages:2.0:Error" ], "status": "503", "detail": "SCIM provisioning is disabled for this tenant" } ``` --- # Get a schema `GET https://api.agilitycredit.net/scim/v2/{tenantId}/Schemas/{schemaId}` Docs page: https://apidocs.agilitycredit.com/sso/scim-api/get-schema · Tag: Discovery Fetch the live definition of a single SCIM schema. Use the canonical schema URI as the path parameter: `urn:ietf:params:scim:schemas:core:2.0:User` or `urn:ietf:params:scim:schemas:core:2.0:Group`. Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `schemaId` | path | yes | The canonical SCIM schema URI. | ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | The requested schema | Schema | | 404 | Resource not found. | Error | | 503 | SCIM provisioning is disabled on the tenant. | Error | ### Error bodies 404: ```json { "schemas": [ "urn:ietf:params:scim:api:messages:2.0:Error" ], "status": "404", "detail": "User not found" } ``` 503: ```json { "schemas": [ "urn:ietf:params:scim:api:messages:2.0:Error" ], "status": "503", "detail": "SCIM provisioning is disabled for this tenant" } ``` --- # List users `GET https://api.agilitycredit.net/scim/v2/{tenantId}/Users` Docs page: https://apidocs.agilitycredit.com/sso/scim-api/list-users · Tag: Users Returns the users for the tenant, optionally filtered. Filterable attributes: `userName`, `emails.value`, `name.givenName`, `name.familyName`, `externalId`. Supported operators: `eq`, `co`, `sw`. Pagination parameters (`startIndex`, `count`) are accepted but the endpoint returns the full filtered set in one response (capped at `maxResults=200`). Unsupported filter attributes are logged and the request still succeeds (returning all users). Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `filter` | query | | SCIM filter expression, e.g. `userName eq "jane@acme.com"`. | | `startIndex` | query | | Accepted but not honoured — the full result set is returned. | | `count` | query | | Accepted but not honoured — the full result set is returned (capped at 200). | ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | List of users | UserListResponse | | 503 | SCIM provisioning is disabled on the tenant. | Error | ### Error bodies 503: ```json { "schemas": [ "urn:ietf:params:scim:api:messages:2.0:Error" ], "status": "503", "detail": "SCIM provisioning is disabled for this tenant" } ``` --- # Create user `POST https://api.agilitycredit.net/scim/v2/{tenantId}/Users` Docs page: https://apidocs.agilitycredit.com/sso/scim-api/create-user · Tag: Users Provision a new user. `userName` must be the user's work email and is **immutable** once set. Returns: - `201 Created` — new user provisioned. - `200 OK` — user already existed (or had been deactivated) and was adopted or reactivated by SCIM. Treat both as success; retries are idempotent. Authentication: `Authorization: Bearer `. ## Request body (`application/scim+json`), required Schema: `User` | Field | Type | Required | Description | | --- | --- | --- | --- | | `schemas` | array of string | yes | | | `id` | string | | Server-assigned UUID. Use it for all subsequent operations. | | `externalId` | string | | Client-assigned correlation ID. Stored verbatim. | | `userName` | string (email) | yes | The user's work email. **Immutable** after creation — attempts to change it via PUT/PATCH are silently ignored. | | `name` | object | yes | | | `name.givenName` | string | yes | | | `name.familyName` | string | yes | | | `name.formatted` | string | | Computed by the server. | | `emails` | array of object | | Primary work email. The `value` must match `userName` and is immutable. | | `emails[].value` | string (email) | yes | | | `emails[].type` | string: `work` | | | | `emails[].primary` | boolean | | | | `phoneNumbers` | array of object | | Phone numbers. `value` must be E.164 (e.g. `+14155550100`). | | `phoneNumbers[].value` | string | yes | E.164 phone number, e.g. `+14155550100`. | | `phoneNumbers[].type` | string: `work`, `mobile` | | | | `phoneNumbers[].primary` | boolean | | | | `roles` | array of string: `admin`, `user`, `sales`, `sales_with_credit`, `api`, `accounting` | | Roles assigned to the user. Only honoured when the tenant's role mapping source is `idp_attribute`. The attribute name is tenant-configurable; `roles` is the default. | | `active` | boolean | yes | `false` deactivates (soft-delete); `true` reactivates a previously deactivated user. | | `meta` | object | | | | `meta.resourceType` | string: `User`, `Group`, `Schema`, `ServiceProviderConfig` | | | | `meta.created` | string (date-time) | | | | `meta.lastModified` | string (date-time) | | | | `meta.location` | string | | | Example: ```json { "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" } ``` ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | User already existed and was adopted or reactivated. | User | | 201 | User created. | User | | 400 | Payload validation failed. | Error | | 503 | SCIM provisioning is disabled on the tenant. | Error | ### Error bodies 400: ```json { "schemas": [ "urn:ietf:params:scim:api:messages:2.0:Error" ], "status": "400", "scimType": "invalidValue", "detail": "userName or emails[0].value is required" } ``` 503: ```json { "schemas": [ "urn:ietf:params:scim:api:messages:2.0:Error" ], "status": "503", "detail": "SCIM provisioning is disabled for this tenant" } ``` --- # Get user `GET https://api.agilitycredit.net/scim/v2/{tenantId}/Users/{userId}` Docs page: https://apidocs.agilitycredit.com/sso/scim-api/get-user · Tag: Users Returns the user resource. `404` if the user doesn't exist or is deactivated. Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `userId` | path | yes | Server-assigned user id, returned in the `id` field on creation. | ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | The user | User | | 404 | Resource not found. | Error | | 503 | SCIM provisioning is disabled on the tenant. | Error | ### Error bodies 404: ```json { "schemas": [ "urn:ietf:params:scim:api:messages:2.0:Error" ], "status": "404", "detail": "User not found" } ``` 503: ```json { "schemas": [ "urn:ietf:params:scim:api:messages:2.0:Error" ], "status": "503", "detail": "SCIM provisioning is disabled for this tenant" } ``` --- # Replace user `PUT https://api.agilitycredit.net/scim/v2/{tenantId}/Users/{userId}` Docs page: https://apidocs.agilitycredit.com/sso/scim-api/replace-user · Tag: Users Full-resource replace. Send the complete user representation. `userName` and `emails` are immutable — different values are silently ignored. Setting `active: false` deactivates the user (equivalent to `DELETE`). Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `userId` | path | yes | Server-assigned user id, returned in the `id` field on creation. | ## Request body (`application/scim+json`), required Schema: `User` | Field | Type | Required | Description | | --- | --- | --- | --- | | `schemas` | array of string | yes | | | `id` | string | | Server-assigned UUID. Use it for all subsequent operations. | | `externalId` | string | | Client-assigned correlation ID. Stored verbatim. | | `userName` | string (email) | yes | The user's work email. **Immutable** after creation — attempts to change it via PUT/PATCH are silently ignored. | | `name` | object | yes | | | `name.givenName` | string | yes | | | `name.familyName` | string | yes | | | `name.formatted` | string | | Computed by the server. | | `emails` | array of object | | Primary work email. The `value` must match `userName` and is immutable. | | `emails[].value` | string (email) | yes | | | `emails[].type` | string: `work` | | | | `emails[].primary` | boolean | | | | `phoneNumbers` | array of object | | Phone numbers. `value` must be E.164 (e.g. `+14155550100`). | | `phoneNumbers[].value` | string | yes | E.164 phone number, e.g. `+14155550100`. | | `phoneNumbers[].type` | string: `work`, `mobile` | | | | `phoneNumbers[].primary` | boolean | | | | `roles` | array of string: `admin`, `user`, `sales`, `sales_with_credit`, `api`, `accounting` | | Roles assigned to the user. Only honoured when the tenant's role mapping source is `idp_attribute`. The attribute name is tenant-configurable; `roles` is the default. | | `active` | boolean | yes | `false` deactivates (soft-delete); `true` reactivates a previously deactivated user. | | `meta` | object | | | | `meta.resourceType` | string: `User`, `Group`, `Schema`, `ServiceProviderConfig` | | | | `meta.created` | string (date-time) | | | | `meta.lastModified` | string (date-time) | | | | `meta.location` | string | | | ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | User replaced | User | | 400 | Payload validation failed. | Error | | 404 | Resource not found. | Error | | 503 | SCIM provisioning is disabled on the tenant. | Error | ### Error bodies 400: ```json { "schemas": [ "urn:ietf:params:scim:api:messages:2.0:Error" ], "status": "400", "scimType": "invalidValue", "detail": "userName or emails[0].value is required" } ``` 404: ```json { "schemas": [ "urn:ietf:params:scim:api:messages:2.0:Error" ], "status": "404", "detail": "User not found" } ``` 503: ```json { "schemas": [ "urn:ietf:params:scim:api:messages:2.0:Error" ], "status": "503", "detail": "SCIM provisioning is disabled for this tenant" } ``` --- # Update user `PATCH https://api.agilitycredit.net/scim/v2/{tenantId}/Users/{userId}` Docs page: https://apidocs.agilitycredit.com/sso/scim-api/patch-user · Tag: Users Partial update via SCIM PATCH. Accepts both pathed and Okta-style no-path operations (the server normalises no-path ops into pathed ops). See the [custom integration guide](https://apidocs.agilitycredit.com/sso/scim-provisioning/custom-integration#update-user--patch-usersid) for the full table of supported paths. Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `userId` | path | yes | Server-assigned user id, returned in the `id` field on creation. | ## Request body (`application/scim+json`), required Schema: `PatchRequest` | Field | Type | Required | Description | | --- | --- | --- | --- | | `schemas` | array of string | yes | | | `Operations` | array of object | yes | | | `Operations[].op` | string: `add`, `replace`, `remove` | yes | | | `Operations[].path` | string | | SCIM path. Optional for `replace`/`add` when `value` is an object — the server normalises Okta-style no-path ops into pathed ops. | | `Operations[].value` | any | | Scalar, object, or array — shape depends on `path`. For role updates this also accepts `{ value: "" }` (Okta `SingleAppRoleAssignment`). | Example: ```json { "schemas": [ "urn:ietf:params:scim:api:messages:2.0:PatchOp" ], "Operations": [ { "op": "replace", "path": "name.givenName", "value": "Janet" }, { "op": "replace", "path": "active", "value": false } ] } ``` ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | User updated | User | | 400 | Payload validation failed. | Error | | 404 | Resource not found. | Error | | 503 | SCIM provisioning is disabled on the tenant. | Error | ### Error bodies 400: ```json { "schemas": [ "urn:ietf:params:scim:api:messages:2.0:Error" ], "status": "400", "scimType": "invalidValue", "detail": "userName or emails[0].value is required" } ``` 404: ```json { "schemas": [ "urn:ietf:params:scim:api:messages:2.0:Error" ], "status": "404", "detail": "User not found" } ``` 503: ```json { "schemas": [ "urn:ietf:params:scim:api:messages:2.0:Error" ], "status": "503", "detail": "SCIM provisioning is disabled for this tenant" } ``` --- # Deactivate user `DELETE https://api.agilitycredit.net/scim/v2/{tenantId}/Users/{userId}` Docs page: https://apidocs.agilitycredit.com/sso/scim-api/delete-user · Tag: Users Soft-deletes (deactivates) the user. The record is retained so a subsequent `POST /Users` with the same `userName` reactivates the user rather than creating a duplicate. Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `userId` | path | yes | Server-assigned user id, returned in the `id` field on creation. | ## Responses | Status | Description | Body | | --- | --- | --- | | 204 | User deactivated. | | | 404 | Resource not found. | Error | | 503 | SCIM provisioning is disabled on the tenant. | Error | ### Error bodies 404: ```json { "schemas": [ "urn:ietf:params:scim:api:messages:2.0:Error" ], "status": "404", "detail": "User not found" } ``` 503: ```json { "schemas": [ "urn:ietf:params:scim:api:messages:2.0:Error" ], "status": "503", "detail": "SCIM provisioning is disabled for this tenant" } ``` --- # List groups `GET https://api.agilitycredit.net/scim/v2/{tenantId}/Groups` Docs page: https://apidocs.agilitycredit.com/sso/scim-api/list-groups · Tag: Groups Returns the virtual groups derived from the tenant's role mapping. Optional `filter` parameter — only `displayName eq ""` is honoured. Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `filter` | query | | SCIM filter expression. Only `displayName eq ""` is honoured. | ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | List of groups | GroupListResponse | | 503 | SCIM provisioning is disabled on the tenant. | Error | ### Error bodies 503: ```json { "schemas": [ "urn:ietf:params:scim:api:messages:2.0:Error" ], "status": "503", "detail": "SCIM provisioning is disabled for this tenant" } ``` --- # Create group (idempotent) `POST https://api.agilitycredit.net/scim/v2/{tenantId}/Groups` Docs page: https://apidocs.agilitycredit.com/sso/scim-api/create-group · Tag: Groups Agility's groups are virtual. A `displayName` matching a configured IdP group returns the existing virtual group with `201`. An unknown name returns `404`. No new mapping is created. Authentication: `Authorization: Bearer `. ## Request body (`application/scim+json`), required Schema: `Group` | Field | Type | Required | Description | | --- | --- | --- | --- | | `schemas` | array of string | yes | | | `id` | string | | The role name (e.g. `admin`). Server-managed. | | `displayName` | string | yes | The IdP group name as configured in the tenant's role mapping. Cannot be changed via SCIM. | | `members` | array of object | | | | `members[].value` | string | yes | The user's `id`. | | `members[].display` | string | | The user's email. | | `meta` | object | | | | `meta.resourceType` | string: `User`, `Group`, `Schema`, `ServiceProviderConfig` | | | | `meta.created` | string (date-time) | | | | `meta.lastModified` | string (date-time) | | | | `meta.location` | string | | | ## Responses | Status | Description | Body | | --- | --- | --- | | 201 | Existing virtual group. | Group | | 400 | Payload validation failed. | Error | | 404 | Resource not found. | Error | | 503 | SCIM provisioning is disabled on the tenant. | Error | ### Error bodies 400: ```json { "schemas": [ "urn:ietf:params:scim:api:messages:2.0:Error" ], "status": "400", "scimType": "invalidValue", "detail": "userName or emails[0].value is required" } ``` 404: ```json { "schemas": [ "urn:ietf:params:scim:api:messages:2.0:Error" ], "status": "404", "detail": "User not found" } ``` 503: ```json { "schemas": [ "urn:ietf:params:scim:api:messages:2.0:Error" ], "status": "503", "detail": "SCIM provisioning is disabled for this tenant" } ``` --- # Get group `GET https://api.agilitycredit.net/scim/v2/{tenantId}/Groups/{groupId}` Docs page: https://apidocs.agilitycredit.com/sso/scim-api/get-group · Tag: Groups The path parameter is the role name (`admin`, `user`, `sales`, etc.), which is the group's `id`. `404` if the role isn't mapped. Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `groupId` | path | yes | The role name (`admin`, `user`, `sales`, `sales_with_credit`, `api`, `accounting`). | ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | The group | Group | | 404 | Resource not found. | Error | | 503 | SCIM provisioning is disabled on the tenant. | Error | ### Error bodies 404: ```json { "schemas": [ "urn:ietf:params:scim:api:messages:2.0:Error" ], "status": "404", "detail": "User not found" } ``` 503: ```json { "schemas": [ "urn:ietf:params:scim:api:messages:2.0:Error" ], "status": "503", "detail": "SCIM provisioning is disabled for this tenant" } ``` --- # Replace group membership `PUT https://api.agilitycredit.net/scim/v2/{tenantId}/Groups/{groupId}` Docs page: https://apidocs.agilitycredit.com/sso/scim-api/replace-group · Tag: Groups Treats the `members` array as the new full membership set. Users not listed lose the role; users newly listed gain it. Only SCIM-provisioned users are touched. No-op when the role mapping source is not `idp_groups` (returns `200`). Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `groupId` | path | yes | The role name (`admin`, `user`, `sales`, `sales_with_credit`, `api`, `accounting`). | ## Request body (`application/scim+json`), required Schema: `Group` | Field | Type | Required | Description | | --- | --- | --- | --- | | `schemas` | array of string | yes | | | `id` | string | | The role name (e.g. `admin`). Server-managed. | | `displayName` | string | yes | The IdP group name as configured in the tenant's role mapping. Cannot be changed via SCIM. | | `members` | array of object | | | | `members[].value` | string | yes | The user's `id`. | | `members[].display` | string | | The user's email. | | `meta` | object | | | | `meta.resourceType` | string: `User`, `Group`, `Schema`, `ServiceProviderConfig` | | | | `meta.created` | string (date-time) | | | | `meta.lastModified` | string (date-time) | | | | `meta.location` | string | | | ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | Group updated. | Group | | 404 | Resource not found. | Error | | 503 | SCIM provisioning is disabled on the tenant. | Error | ### Error bodies 404: ```json { "schemas": [ "urn:ietf:params:scim:api:messages:2.0:Error" ], "status": "404", "detail": "User not found" } ``` 503: ```json { "schemas": [ "urn:ietf:params:scim:api:messages:2.0:Error" ], "status": "503", "detail": "SCIM provisioning is disabled for this tenant" } ``` --- # Patch group membership `PATCH https://api.agilitycredit.net/scim/v2/{tenantId}/Groups/{groupId}` Docs page: https://apidocs.agilitycredit.com/sso/scim-api/patch-group · Tag: Groups Supports `add`, `remove`, and `replace` operations targeting `members`. Okta-style targeted remove (`"path": "members[value eq \"\"]"`) works as well as the unpathed `remove` of the entire `members` collection. `displayName` changes are ignored. No-op when the role mapping source is not `idp_groups`. Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `groupId` | path | yes | The role name (`admin`, `user`, `sales`, `sales_with_credit`, `api`, `accounting`). | ## Request body (`application/scim+json`), required Schema: `PatchRequest` | Field | Type | Required | Description | | --- | --- | --- | --- | | `schemas` | array of string | yes | | | `Operations` | array of object | yes | | | `Operations[].op` | string: `add`, `replace`, `remove` | yes | | | `Operations[].path` | string | | SCIM path. Optional for `replace`/`add` when `value` is an object — the server normalises Okta-style no-path ops into pathed ops. | | `Operations[].value` | any | | Scalar, object, or array — shape depends on `path`. For role updates this also accepts `{ value: "" }` (Okta `SingleAppRoleAssignment`). | Example: ```json { "schemas": [ "urn:ietf:params:scim:api:messages:2.0:PatchOp" ], "Operations": [ { "op": "add", "path": "members", "value": [ { "value": "6f0e9c6a-1b5a-4f0a-8e3b-1a2c3d4e5f60" } ] }, { "op": "remove", "path": "members[value eq \"a1b2c3d4-...\"]" } ] } ``` ## Responses | Status | Description | Body | | --- | --- | --- | | 200 | Group updated. | Group | | 400 | Payload validation failed. | Error | | 404 | Resource not found. | Error | | 503 | SCIM provisioning is disabled on the tenant. | Error | ### Error bodies 400: ```json { "schemas": [ "urn:ietf:params:scim:api:messages:2.0:Error" ], "status": "400", "scimType": "invalidValue", "detail": "userName or emails[0].value is required" } ``` 404: ```json { "schemas": [ "urn:ietf:params:scim:api:messages:2.0:Error" ], "status": "404", "detail": "User not found" } ``` 503: ```json { "schemas": [ "urn:ietf:params:scim:api:messages:2.0:Error" ], "status": "503", "detail": "SCIM provisioning is disabled for this tenant" } ``` --- # Clear group membership `DELETE https://api.agilitycredit.net/scim/v2/{tenantId}/Groups/{groupId}` Docs page: https://apidocs.agilitycredit.com/sso/scim-api/delete-group · Tag: Groups Removes the role from every SCIM-provisioned user that holds it, but does not remove the underlying mapping. No-op when the role mapping source is not `idp_groups` (returns `204`). Authentication: `Authorization: Bearer `. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `groupId` | path | yes | The role name (`admin`, `user`, `sales`, `sales_with_credit`, `api`, `accounting`). | ## Responses | Status | Description | Body | | --- | --- | --- | | 204 | Group membership cleared. | | | 404 | Resource not found. | Error | | 503 | SCIM provisioning is disabled on the tenant. | Error | ### Error bodies 404: ```json { "schemas": [ "urn:ietf:params:scim:api:messages:2.0:Error" ], "status": "404", "detail": "User not found" } ``` 503: ```json { "schemas": [ "urn:ietf:params:scim:api:messages:2.0:Error" ], "status": "503", "detail": "SCIM provisioning is disabled for this tenant" } ``` --- # SSO-exempt users Once SSO is enabled, every member of your tenant signs in through your IdP. If your IdP is down or misconfigured, that means nobody can sign in, including the administrators who would fix it. SSO-exempt users are your **emergency access**: a small number of members who can still sign in with their email and password while everyone else uses SSO. Manage them under **Settings → Account → Identity → SSO**, in the **SSO-exempt users** section. :::tip Add one before you enable SSO You can exempt users before SSO is turned on. Do it first, so a mistake in your IdP setup can't lock every administrator out on the day you go live. ::: ## Who can be exempt - Any member of your tenant, whatever their role, including users provisioned by SCIM. Administrators can exempt themselves. - Only tenant administrators can add or remove an exemption. - Every time a user is added to or removed from the list, including the automatic removals below, an email goes to all of your tenant's administrators. Keep the list short: an exempt user bypasses the controls your IdP enforces, such as conditional access and off-boarding. :::caution Prefer accounts your IdP doesn't manage Emergency access should not depend on the IdP it stands in for. If SCIM provisions an exempt user, your IdP can still deactivate them, which removes the exemption, or change their roles. The portal marks SCIM-provisioned users in the list. To take a user out of SCIM, use **Remove provisioning** under **Settings → Account → Users**, and leave them unassigned from the Agility app in your IdP. ::: ## When an exemption is removed automatically | What happens | Result | | ------------------------------------------------------------------------ | -------------------------------------------------------------------- | | Your IdP creates or updates the user over SCIM, including their roles | The exemption stays | | The user is deactivated or deleted over SCIM, or removed from the tenant | The exemption is removed; a user added back later starts without one | When an administrator removes an exemption while SSO is enabled, the user's open password sessions in the tenant end, and their SSO sessions carry on. When the user is removed from the tenant, all of their sessions there end. Source: https://apidocs.agilitycredit.com/sso/sso-exempt-users --- # SSO & SCIM Troubleshooting This page covers common problems and how to use the activity log to diagnose them. ## The activity log All SSO and SCIM events are recorded under **Settings → Account → Identity → Activity**. The log shows the last 7 days, filterable by event type. **Always-on events:** | Event type | When it fires | | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | `sso_error` | Anything that went wrong during sign-in | | `sso_presignup_reject` | A user was blocked at pre-signup — typically SCIM is enabled and the user hasn't been provisioned yet, or SSO isn't enabled on the tenant | | `sso_user_created` | A new Agility user was JIT-created on first SSO sign-in | | `sso_user_attributes_updated` | An existing user's name, phone, or roles were re-synced from IdP claims | | `scim_error` | A SCIM request failed | | `scim_provision` | A user was created or updated via SCIM | | `scim_deprovision` | A user was deactivated via SCIM | **Debug-only events** (enable **Log successful events (debug)** to see these): | Event type | When it fires | | ------------------- | -------------------------------------------------------------------------------------------------------------- | | `sso_login_success` | A user successfully signed in via SSO (one event per sign-in, regardless of whether anything changed) | | `scim_read` | A SCIM read request with full request/response payload | Always-on events cover every mutation — user creates, attribute/role changes, SCIM writes, errors. Debug mode adds visibility into no-op sign-ins and SCIM reads, which are higher-volume and lower-audit-value. Turn debug off after go-live to keep the log readable. Click any row in the table to expand it and see the underlying detail — claim values, request bodies, error messages, HTTP status codes. --- ## SSO problems ### `sso_presignup_reject` — SCIM required, user not provisioned **Cause:** The tenant has SCIM enabled, and the user signing in does not yet exist in Agility. JIT user creation is disabled when SCIM is on. **Fix:** - Make sure the user is assigned to the Agility app in your IdP. - Trigger a sync from your IdP (Okta and Entra usually run automatically every 40 minutes). - Check the **Activity** tab for a corresponding `scim_provision` event. - Have the user retry. ### Users sign in but have no roles **Cause:** None of the IdP groups (or attribute values) sent by your IdP match any row in your role mapping table. **Fix:** 1. Enable **Log successful events (debug)** on the **Activity** tab. 2. Have the user sign in again. 3. Open the `sso_login_success` event (or `sso_user_attributes_updated` if roles changed) and inspect the claim values your IdP actually sent. 4. Check that: - Your IdP is including a `groups` claim (or the attribute you configured). - The values match your role mapping table **exactly** — matching is case-sensitive. Common gotchas: - **Entra sends group object IDs (GUIDs) by default**, not group names. The recommended fix is to switch to **App Roles** — see [Set up Entra ID](https://apidocs.agilitycredit.com/sso/setup-entra). As a quicker workaround, set the group claim's **Source attribute** to **Cloud-only group display names** (cloud-only Entra groups) or **sAMAccountName** (on-prem AD-synced groups). Pasting raw GUIDs into your role mapping table works for SAML and OIDC but **does not work for SCIM tenants** — SCIM matches groups by display name, not object ID. - Okta sends only the groups that match the filter in the **Group Attribute Statement** — if the filter is too narrow, expected groups won't appear. ### given_name and family_name are missing or empty **Cause:** Your IdP isn't sending `given_name` / `family_name` claims under those exact names, or isn't sending them at all. **Fix:** - **Okta SAML:** In **Attribute Statements**, add `given_name → user.firstName` and `family_name → user.lastName`. - **Okta OIDC:** Make sure the `profile` scope is granted; Okta sends `given_name` / `family_name` automatically in that scope. - **Entra SAML:** In **Attributes & Claims**, ensure `given_name` is mapped to `user.givenname` and `family_name` to `user.surname`. - **Entra OIDC:** Make sure the `profile` permission is granted with admin consent. If your IdP can't be changed but does send the names under different claim names (e.g. `firstName`, `lastName`), contact your Agility account team — we can configure a per-tenant attribute mapping override. ### "We couldn't sign you in" on the OTP step **Cause:** Either the wrong code was entered, the code expired (10 minutes), or the IdP discovery service is unreachable. **Fix:** - Have the user request a new code and try again. - If the error mentions discovery, check the Activity tab for `sso_error` events; this usually indicates a misconfigured tenant SSO setting. ### Multi-tenant user can't pick a tenant **Cause:** The user belongs to more than one SSO-enabled tenant. By design, they see a tenant picker after entering the OTP. They must pick one before being redirected to the IdP. **Fix:** No action needed — this is expected. Confirm the user actually has access to the tenant they're trying to reach. ### An SSO-exempt user can't sign in with their password **Cause:** The exemption no longer applies. Check the portal's **Audit logs** page for an **SSO exemption removed** entry. The usual reasons are: - Your IdP deactivated or deleted the user over SCIM, which removes them from the tenant and their exemption with it. Keep emergency access accounts out of the users and groups assigned to the Agility app. - The user was removed from the tenant and added back, which starts them without an exemption. **Fix:** Add them again under [SSO-exempt users](https://apidocs.agilitycredit.com/sso/sso-exempt-users). If SCIM manages them, consider **Remove provisioning** (**Settings → Account → Users**) so your IdP can't deactivate them again. ### Test Connection passes but real sign-in fails **Cause:** The **Test Connection** button validates configuration _without_ invoking your IdP — it checks that URLs are reachable, certificates parse, and the discovery document is well-formed. Real sign-in can still fail if: - The user isn't assigned to the Agility app in your IdP. - Your IdP rejects the redirect URI (typo in what you pasted, or extra trailing slash). - Your IdP doesn't actually send the claims Agility expects (see "Users sign in but have no roles"). **Fix:** 1. Have a real user attempt to sign in. 2. Check the **Activity** tab for `sso_error` events. 3. Read the detail row — it includes the underlying error from the IdP or Agility's sign-in service. --- ## SCIM problems ### SCIM requests return 401 Unauthorized **Cause:** The bearer token (API key) in your IdP's SCIM configuration is missing, expired, or doesn't match an active key. **Fix:** - In Agility, go to **Settings → Account → API Keys** and confirm the key is still active. - If you've rotated keys, update the token in your IdP's SCIM connector. - For Okta: **Provisioning → To App → Edit → API Token**. - For Entra: **Provisioning → Admin Credentials → Secret Token**. ### SCIM requests return 404 or wrong tenant **Cause:** The SCIM endpoint URL in your IdP is pointing at the wrong tenant ID, or includes a typo. **Fix:** Copy the SCIM endpoint from the portal again — use the copy button on the **SCIM** tab. The URL must include your tenant's exact ID after `/scim/v2/`. ### Users provision but with no roles **Cause:** Same root cause as the SSO version: SCIM isn't sending group memberships (or the configured attribute), or values don't match the role mapping table. **Fix:** 1. Enable debug logging on the **Activity** tab. 2. Inspect a `scim_read` or `scim_provision` event after the next sync. 3. Confirm the group/attribute values your IdP is sending. 4. Adjust the role mapping table to match. ### A user was removed in the IdP but still has access **Cause:** SCIM deprovisioning hasn't run yet, or the user was unassigned from the app without being deactivated. **Fix:** - Trigger a manual SCIM sync from your IdP. - Check the **Activity** tab for a `scim_deprovision` event. - If the event doesn't appear, confirm your IdP is configured to send a deactivation (Okta: enable **Deactivate Users** under provisioning settings; Entra: ensure the user is **unassigned** from the app, not just removed from a group). --- ## SAML signing problems ### "SAML validation failed: Invalid document signature" **Cause (most common — Entra):** Entra is signing only the outer SAML response and not the assertion inside it. Agility requires the assertion to be signed. **Fix (Entra):** 1. In your Entra enterprise application, open **Single sign-on**. 2. In the **SAML Certificates** section, click the **edit (pencil)** icon at the top right. 3. Change **Signing Option** from **Sign SAML response** to **Sign SAML response and assertion**. 4. Leave **Signing Algorithm** at **SHA-256**. 5. **Save** and retry sign-in. **Other causes worth checking:** - The certificate pasted into Agility doesn't match the one the IdP is signing with (e.g. the IdP rotated keys after you configured Agility). Re-import the metadata URL to refresh the certificate. - The certificate is correct but truncated or has whitespace inside the Base64 body. Re-paste the certificate as a single contiguous Base64 block (PEM headers optional). - The IdP is signing with SHA-1 in an environment that requires SHA-256, or vice versa. Standardize on **SHA-256**. ### Sign-in fails after IdP rotates the SAML certificate **Cause:** Your IdP started signing assertions with a new certificate, but Agility only has the old one. **Fix:** - Before rotation, paste the **next** certificate into the **Signing Certificates** field in Agility using the **+** icon next to the certificate list. Agility accepts assertions signed by any of the configured certificates, so both old and new will work during the rollover window. - After your IdP cuts over, remove the old certificate. If you missed the rotation window, simply re-import the metadata URL — Agility will fetch the current certificate from your IdP and replace the stored one. --- ## Still stuck If you've worked through the above and SSO or SCIM still isn't working: 1. Reproduce the failure with a single test user. 2. Note the approximate timestamp of the attempt. 3. Capture the relevant entry from the **Activity** tab (screenshot or copy the detail JSON). 4. Open a support request and include the timestamp, tenant ID, user email, and the captured event detail. We officially support Okta and Entra ID for both SSO and SCIM. Other standards-compliant IdPs may work but are best-effort. Source: https://apidocs.agilitycredit.com/sso/troubleshooting --- # Creating Webhooks Webhooks allow your system to receive real-time notifications when events occur in the platform (for example when a PID, OTP, or compliance check is completed). Webhooks can be configured from the portal under: > **Settings → Account → Webhooks** ## Step 1: Open the Webhooks Page 1. Log in to the portal. 2. Open **Settings** from the main navigation. 3. Select **Account**. 4. Click **Webhooks**. You will see an overview of all webhooks configured for your account. *(Screenshot: Webhooks overview)* ## Step 2: Create a New Webhook 1. Click the **Create Webhook** button. 2. Fill in the webhook configuration fields: ### Configuration Fields - **Enable webhook** Enables or disables the webhook. Disabled webhooks will not receive events. - **Name** A human-readable name to identify the webhook *(e.g. `Production Webhook`, `Backend Listener`)* - **URL** The HTTPS endpoint that will receive webhook events *(e.g. `https://api.yourdomain.com/webhooks/agc`)* - **Secret** *(optional but recommended)* A shared secret used to sign webhook deliveries with HMAC. If no secret is configured, webhook requests will **not** include the `X-Agc-Signature` header. *(Screenshot: Create webhook configuration)* Under **Events**, choose which events this webhook should receive. You can select: * **All events** – receive notifications for every available event * **Selected events** – receive notifications only for specific events When selecting specific events, you can subscribe to events such as: * `pid.completed` * `otp.completed` * `compliance.cleared` * `dlv.completed` * `oow.completed` * `applicant_document.created` * `applicant_document.updated` * `applicant_document.deleted` ## Step 3: Save the Webhook 1. Click **Create** to save the webhook. 2. You will be redirected back to the Webhooks overview page. 3. The new webhook will appear in the list showing: * Name * Endpoint URL * Subscribed events * Last delivery status * Last delivery timestamp * Creation date Once created, the webhook becomes **active immediately** and will start receiving events. *(Screenshot: Select webhook events)* ## Best Practices * Always use **HTTPS** endpoints * Configure a **secret** and validate signatures * Respond with a `2xx` HTTP status code once processing succeeds * Implement **idempotent processing** using `event_id` * Log webhook deliveries for debugging and auditing * Keep webhook endpoints lightweight and fast ## Troubleshooting - **No events received** - Ensure the webhook is enabled - Verify the correct events are selected - Check endpoint reachability - **Signature validation failures** - Ensure the raw request body is used - Confirm the correct secret is configured - Validate timestamp tolerance Source: https://apidocs.agilitycredit.com/webhooks/create-webhooks --- # Handle Deliveries This document describes how webhook events are delivered, acknowledged, retried, and how consumers should process and order them. ## Overview Webhook events are delivered using an **at-least-once delivery model**. This means: > **Each event is guaranteed to be delivered one or more times.** > Duplicate deliveries are possible and expected. Webhook consumers **must implement idempotent processing**. ## Delivery Model - **Delivery guarantee:** At least once - **Duplicates:** Possible - **Ordering:** Not guaranteed at delivery time ## Delivery Method - **Protocol:** HTTPS only - **HTTP Method:** `POST` - **Content-Type:** `application/json` - **Encoding:** UTF-8 ## Delivery Headers Each delivery includes the following headers: | Header | Description | | ------------------ | -------------------------------------------------------------------------------------- | | `X-Agc-Version` | Webhook payload version | | `X-Agc-Tenant-Id` | ID of the tenant that triggered the event | | `X-Agc-Webhook-Id` | ID of the configured webhook | | `X-Agc-Attempt-Id` | Unique identifier for **this delivery attempt** | | `X-Agc-Event-Id` | Unique event identifier (stable across retries, use for idempotency) | | `X-Agc-Event-Type` | Name of the event type (e.g. `pid.completed`) | | `X-Agc-Timestamp` | ISO-8601 timestamp indicating **when this delivery attempt was sent** | | `X-Agc-Signature` | HMAC signature used to verify authenticity (**only present if a secret is configured** | ## Delivery Payload The webhook request body uses the following structure: ```json { "version": "1.0", "tenant_id": "tenant_abc", "event_id": "evt_123456789", "event_type": "pid.completed", "event_timestamp": "2026-01-22T06:40:00.000Z", "data": {} } ``` ### Payload Field Semantics | Field | Description | | ----------------- | ----------------------------------------------- | | `version` | Webhook payload version | | `tenant_id` | ID of the tenant that triggered the event | | `event_id` | Unique event identifier (stable across retries) | | `event_type` | Event type name | | `event_timestamp` | When the event occurred in the platform | | `data` | Event-specific payload | --- ## Success & Failure Criteria ### Successful Delivery Any HTTP response with a status code in the range: ``` 200–399 ``` is considered a **successful delivery**, and **no further retries will be made**. ### Retryable Failures The following status codes **will trigger retries**: - `408` – Request Timeout - `429` – Too Many Requests - `500–599` – Server errors Retries are also triggered for: - Network errors - TLS failures - DNS failures - Connection timeouts ### Non-Retryable Failures The following status codes **will NOT be retried**: - `400` – Bad Request - `401` – Unauthorized - `403` – Forbidden - `404` – Not Found - `410` – Gone When these responses are received, the delivery is considered **final**. --- ## Retry Behavior ### Retry Strategy - Retries use **exponential backoff** - Retries continue for **up to 24 hours** - Retry intervals are not guaranteed and may vary ### Retry Characteristics - `event_id` remains **constant** across all retries - `attempt_id` is **unique** for every delivery attempt - The same payload is resent for each retry --- ## Idempotency (Required) Due to at-least-once delivery semantics, webhook consumers **must implement idempotent processing**. ### Idempotency Key Use: ``` event_id ``` ### Recommended Processing Pattern ```text if event_id already processed: return success else: process event store event_id ``` Failing to implement idempotency may result in duplicate side effects. --- ## Timeout Expectations Webhook endpoints should: - Respond within a few seconds - Avoid long-running synchronous processing - Offload heavy work to background jobs or queues Slow responses increase retry likelihood. --- ## Security Considerations - Use HTTPS - Validate HMAC signatures when present - Validate timestamps (`X-Agc-Timestamp`) to prevent replay attacks - Reject malformed payloads See [**Validating Deliveries**](https://apidocs.agilitycredit.com/webhooks/validating-deliveries) for details. --- ## Logging & Observability Strongly recommended logging fields: - `event_id` - `event_type` - `attempt_id` - `event_timestamp` - `X-Agc-Timestamp` - HTTP response codes - Signature verification failures Source: https://apidocs.agilitycredit.com/webhooks/handle-deliveries --- # Validating Deliveries To ensure webhook deliveries are authentic and untampered, webhook requests can be signed using **HMAC**. Signature verification is **optional** and only applies when a **secret** is configured on the webhook. ## When Is a Signature Included? - ✅ If a webhook **has a secret configured**, requests **will include** the `X-Agc-Signature` header - ❌ If **no secret is configured**, the `X-Agc-Signature` header **will NOT be included** ## Signature Headers When enabled, the following headers are used for verification: ```http X-Agc-Signature: X-Agc-Timestamp: ``` - `X-Agc-Signature` is the computed HMAC - `X-Agc-Timestamp` is included in the signed message to prevent replay attacks ## Signature Construction The signature is generated using: - **Algorithm**: `HMAC-SHA256` - **Secret**: The webhook secret you configured in the portal - **Signed value**: A concatenation of the timestamp and the raw request body ### Signed Payload Format ``` . ``` Example: ``` 2026-01-22T06:40:00.000Z.{"version":"1.0","tenant_id":"tenant_abc",...} ``` > ⚠️ The **raw request body must be used exactly as received** > Do not parse, reformat, pretty-print, or reorder JSON keys before verification. --- ## Verification Steps ### 1. Read Required Values From the incoming request: - `X-Agc-Signature` - `X-Agc-Timestamp` - Raw request body (string) - Your webhook secret ### 2. Recreate the Signed Payload ```text signed_payload = X-Agc-Timestamp + "." + raw_body ``` ### 3. Compute the Expected Signature Using HMAC-SHA256: ``` expected_signature = HMAC_SHA256(secret, signed_payload) ``` The resulting signature is **hex-encoded lowercase**. ### 4. Compare Signatures Securely Compare the received signature with the computed signature using a **constant-time comparison**. If they match, the webhook is valid. --- ## Timestamp Validation (Recommended) To protect against replay attacks, validate the timestamp: - Parse `X-Agc-Timestamp` - Reject requests older than a defined window (e.g. **5 minutes**) Example rule: ``` abs(now - timestamp) <= 300 seconds ``` --- ## End-to-End Verification Examples This section demonstrates a complete webhook handling flow using a shared webhook secret. > ⚠️ Always use the **raw request body** exactly as received. **Javascript** ```js const app = express(); const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET; const MAX_SKEW_SECONDS = 5 * 60; // -------------------------------------------------- // Raw body capture (MUST run before signature check) // -------------------------------------------------- app.use( express.json({ verify: (req, _res, buf) => { req.rawBody = buf.toString("utf8"); }, }) ); // -------------------------------------------------- // Helpers // -------------------------------------------------- function getHeader(req, name) { return req.get(name) || undefined; } function computeHmacHex(secret, timestamp, rawBody) { const signedPayload = `${timestamp}.${rawBody}`; return crypto.createHmac("sha256", secret).update(signedPayload, "utf8").digest("hex"); } function safeEqualHex(aHex, bHex) { // timingSafeEqual requires buffers of equal length if (aHex.length !== bHex.length) return false; try { return crypto.timingSafeEqual(Buffer.from(aHex, "hex"), Buffer.from(bHex, "hex")); } catch { // Invalid hex input return false; } } function isTimestampFresh(isoTimestamp, maxSkewSeconds) { const ms = Date.parse(isoTimestamp); if (Number.isNaN(ms)) return false; const ageSeconds = Math.abs(Date.now() - ms) / 1000; return ageSeconds <= maxSkewSeconds; } function verifyAgcSignature({ secret, signature, timestamp, rawBody }) { const expected = computeHmacHex(secret, timestamp, rawBody); return safeEqualHex(signature, expected); } // -------------------------------------------------- // Webhook endpoint // -------------------------------------------------- app.post("/webhooks/agc", (req, res) => { const signature = getHeader(req, "X-Agc-Signature"); const deliveryTimestamp = getHeader(req, "X-Agc-Timestamp"); const eventType = getHeader(req, "X-Agc-Event-Type"); const eventId = getHeader(req, "X-Agc-Event-Id"); // Signature & timestamp required if a secret is configured if (WEBHOOK_SECRET) { if (!signature) return res.status(401).send("Missing signature"); if (!deliveryTimestamp) return res.status(401).send("Missing timestamp"); if (!req.rawBody) return res.status(400).send("Missing raw body"); if (!isTimestampFresh(deliveryTimestamp, MAX_SKEW_SECONDS)) { return res.status(401).send("Expired timestamp"); } const valid = verifyAgcSignature({ secret: WEBHOOK_SECRET, signature, timestamp: deliveryTimestamp, rawBody: req.rawBody, }); if (!valid) return res.status(401).send("Invalid signature"); } // -------------------------------------------------- // Process event (MUST be idempotent) // -------------------------------------------------- const payload = req.body; console.log("Webhook received:", { eventId, eventType, payload, }); return res.sendStatus(200); }); // -------------------------------------------------- // Server // -------------------------------------------------- app.listen(3000, () => { console.log("Webhook listener running on port 3000"); }); ``` **Typescript** ```ts const app = express(); const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET; const MAX_SKEW_SECONDS = 5 * 60; // ---- Raw body capture (must be BEFORE signature verification) ---- type RawBodyRequest = Request & { rawBody?: string }; app.use( express.json({ verify: (req: RawBodyRequest, _res, buf) => { req.rawBody = buf.toString("utf8"); }, }) ); // ---- Helpers ---- const getHeader = (req: Request, name: string): string | undefined => req.get(name) ?? undefined; function computeHmacHex(secret: string, timestamp: string, rawBody: string): string { const signedPayload = `${timestamp}.${rawBody}`; return crypto.createHmac("sha256", secret).update(signedPayload, "utf8").digest("hex"); } function safeEqualHex(aHex: string, bHex: string): boolean { // timingSafeEqual requires equal-length buffers; short-circuit safely if (aHex.length !== bHex.length) return false; try { return crypto.timingSafeEqual(Buffer.from(aHex, "hex"), Buffer.from(bHex, "hex")); } catch { // invalid hex input return false; } } function isTimestampFresh(isoTimestamp: string, maxSkewSeconds: number): boolean { const ms = Date.parse(isoTimestamp); if (Number.isNaN(ms)) return false; const ageSeconds = Math.abs(Date.now() - ms) / 1000; return ageSeconds <= maxSkewSeconds; } function verifyAgcSignature(opts: { secret: string; signature: string; timestamp: string; rawBody: string }): boolean { const expected = computeHmacHex(opts.secret, opts.timestamp, opts.rawBody); return safeEqualHex(opts.signature, expected); } // ---- Route ---- app.post("/webhooks/agc", (req: RawBodyRequest, res: Response) => { const signature = getHeader(req, "X-Agc-Signature"); const deliveryTimestamp = getHeader(req, "X-Agc-Timestamp"); const eventType = getHeader(req, "X-Agc-Event-Type"); const eventId = getHeader(req, "X-Agc-Event-Id"); // If you configured a secret, signature + timestamp are required. if (WEBHOOK_SECRET) { if (!signature) return res.status(401).send("Missing signature"); if (!deliveryTimestamp) return res.status(401).send("Missing timestamp"); if (!req.rawBody) return res.status(400).send("Missing raw body"); if (!isTimestampFresh(deliveryTimestamp, MAX_SKEW_SECONDS)) { return res.status(401).send("Expired timestamp"); } const ok = verifyAgcSignature({ secret: WEBHOOK_SECRET, signature, timestamp: deliveryTimestamp, rawBody: req.rawBody, }); if (!ok) return res.status(401).send("Invalid signature"); } // Process event (make idempotent using event_id from body or X-Agc-Event-Id) const payload = req.body; console.log("Webhook received:", { eventId, eventType, payload, }); return res.sendStatus(200); }); // ---- Server ---- app.listen(3000, () => { console.log("Webhook listener running on port 3000"); }); ``` **Python** ```py from fastapi import FastAPI, Request, HTTPException from datetime import datetime, timezone app = FastAPI() WEBHOOK_SECRET = os.getenv("AGC_WEBHOOK_SECRET") def verify_signature(secret, signature, timestamp, raw_body): payload = f"{timestamp}.{raw_body}".encode("utf-8") expected = hmac.new( secret.encode("utf-8"), payload, hashlib.sha256 ).hexdigest() return hmac.compare_digest(signature, expected) @app.post("/webhooks/agc") async def handle_webhook(request: Request): raw_body = (await request.body()).decode("utf-8") signature = request.headers.get("X-Agc-Signature") timestamp = request.headers.get("X-Agc-Timestamp") event_type = request.headers.get("X-Agc-Event-Type") event_id = request.headers.get("X-Agc-Event-Id") # 1. Signature required if secret exists if WEBHOOK_SECRET and not signature: raise HTTPException(status_code=401, detail="Missing signature") # 2. Timestamp validation (5 minutes) if timestamp: age = abs( (request.state.now if hasattr(request.state, "now") else 0) ) # Simpler timestamp check event_time = datetime.fromisoformat(timestamp.replace("Z", "+00:00")) now = datetime.now(timezone.utc) if abs((now - event_time).total_seconds()) > 300: raise HTTPException(status_code=401, detail="Expired timestamp") # 3. Verify signature if WEBHOOK_SECRET and signature and timestamp: if not verify_signature( WEBHOOK_SECRET, signature, timestamp, raw_body ): raise HTTPException(status_code=401, detail="Invalid signature") # 4. Process event payload = await request.json() print("Webhook received:", { "event_id": event_id, "event_type": event_type, "payload": payload, }) return {"status": "ok"} ``` --- ## Handling Missing Signatures If the `X-Agc-Signature` header is missing: - ✅ Check if the webhook **is configured without a secret** - ❌ If a secret **is configured**, treat the request as **invalid** --- ## Best Practices - Always use **HTTPS** - Store webhook secrets securely - Use **raw request bodies** for verification - Enforce a timestamp tolerance window - Log failed verification attempts - Rotate secrets periodically Source: https://apidocs.agilitycredit.com/webhooks/validating-deliveries --- # Webhook events & payloads ## General Payload Structure All webhook deliveries use a **single, consistent JSON payload structure**. ```json { "version": "1.0", "tenant_id": "tenant_abc", "event_id": "evt_123456789", "event_type": "pid.completed", "event_timestamp": "2026-01-22T06:40:00.000Z", "data": {} } ``` ### Payload Fields | Field | Type | Description | | ----------------- | -------- | --------------------------------------------------------------------- | | `version` | `string` | Webhook payload version | | `tenant_id` | `string` | ID of the tenant that triggered the event | | `event_id` | `string` | Unique event identifier (stable across retries, safe for idempotency) | | `event_type` | `string` | The event type name (e.g. `pid.completed`) | | `event_timestamp` | `string` | ISO-8601 timestamp indicating when the event occurred | | `data` | `object` | Event-specific payload (see event definitions below) | > ℹ️ The `event_type` field always matches the value of the > `X-Agc-Event-Type` request header. ### Relationship Between Headers & Payload Some metadata is available **both in headers and in the payload** for convenience and reliability. | Header | Payload Field | | ------------------ | ------------- | | `X-Agc-Version` | `version` | | `X-Agc-Tenant-Id` | `tenant_id` | | `X-Agc-Event-Id` | `event_id` | | `X-Agc-Event-Type` | `event_type` | The payload body should be treated as the **authoritative source**, while headers allow quick filtering, routing, and logging at the HTTP layer. --- ## Event Types & Payloads This section documents all webhook event types and the structure of the `data` object for each event. ### Applicant Document Events #### `applicant_document.created` Triggered when an applicant document is created. ```json { "version": "1.0", "tenant_id": "tenant_abc", "event_id": "evt_doc_created_001", "event_type": "applicant_document.created", "event_timestamp": "2026-01-22T06:12:00.000Z", "data": { "id": "doc_123", "applicant_id": "applicant_456", "category": "passport" } } ``` #### `applicant_document.updated` Triggered when an applicant document is updated. ```json { "version": "1.0", "tenant_id": "tenant_abc", "event_id": "evt_doc_updated_002", "event_type": "applicant_document.updated", "event_timestamp": "2026-01-22T06:15:00.000Z", "data": { "id": "doc_123", "applicant_id": "applicant_456", "category": "passport" } } ``` #### `applicant_document.deleted` Triggered when an applicant document is deleted. ```json { "version": "1.0", "tenant_id": "tenant_abc", "event_id": "evt_doc_deleted_003", "event_type": "applicant_document.deleted", "event_timestamp": "2026-01-22T06:18:00.000Z", "data": { "id": "doc_123", "applicant_id": "applicant_456", "category": "passport" } } ``` --- ### Compliance Events #### `compliance.cleared` Triggered when a compliance check has been cleared. ```json { "version": "1.0", "tenant_id": "tenant_abc", "event_id": "evt_compliance_010", "event_type": "compliance.cleared", "event_timestamp": "2026-01-22T06:20:00.000Z", "data": { "id": "cmp_789", "applicant_id": "applicant_456", "applicant_type": "applicant_main" } } ``` --- ### Driver License Verification (DLV) #### `dlv.completed` Triggered when a driver license verification is completed. ```json { "version": "1.0", "tenant_id": "tenant_abc", "event_id": "evt_dlv_020", "event_type": "dlv.completed", "event_timestamp": "2026-01-22T06:25:00.000Z", "data": { "id": "dlv_321", "applicant_id": "applicant_456", "applicant_type": "applicant_main" } } ``` --- ### FastApp Events #### `fastapp.submitted` Triggered when an applicant submits a FastApp. ```json { "version": "1.0", "tenant_id": "tenant_abc", "event_id": "evt_fastapp_025", "event_type": "fastapp.submitted", "event_timestamp": "2026-01-22T06:28:00.000Z", "data": { "applicant_id": "applicant_456", "fastapp_id": "fastapp_789" } } ``` --- ### Out of Wallet (OOW) #### `oow.completed` Triggered when an Out of Wallet Questionnaire is completed. ```json { "version": "1.0", "tenant_id": "tenant_abc", "event_id": "evt_oow_030", "event_type": "oow.completed", "event_timestamp": "2026-01-22T06:30:00.000Z", "data": { "id": "oow_654", "applicant_id": "applicant_456", "applicant_type": "applicant_main" } } ``` --- ### One-Time Password (OTP) #### `otp.completed` Triggered when an OTP verification is completed. ```json { "version": "1.0", "tenant_id": "tenant_abc", "event_id": "evt_otp_040", "event_type": "otp.completed", "event_timestamp": "2026-01-22T06:35:00.000Z", "data": { "id": "otp_987", "applicant_id": "applicant_456", "applicant_type": "applicant_main" } } ``` --- ### Precise ID (PID) #### `pid.completed` Triggered when a Precise ID flow is completed. ```json { "version": "1.0", "tenant_id": "tenant_abc", "event_id": "evt_pid_050", "event_type": "pid.completed", "event_timestamp": "2026-01-22T06:40:00.000Z", "data": { "id": "pid_111", "applicant_id": "applicant_456", "applicant_type": "applicant_main" } } ``` --- ### Event Payload Reference #### Common Fields | Field | Description | | ---------------- | ---------------------------------------------------------------- | | `id` | ID of the completed resource (document, verification, PID, etc.) | | `applicant_id` | ID of the related applicant | | `applicant_type` | Type of applicant (e.g. `applicant_main`, `applicant_co`) | | `category` | Document category (documents only) | | `fastapp_id` | ID of the FastApp integration (FastApp events only) | --- ## Important Notes - The `data` field is **event-specific** and its structure depends on `event_type` - Always validate the event using `event_type` before processing `data` - Use `event_id` for **idempotent processing** - `event_timestamp` represents the time the event occurred Source: https://apidocs.agilitycredit.com/webhooks/events-and-payloads --- # Deep links If you're embedding Agility into another application — a partner dashboard, an internal portal, an "Open in Agility" button on a third-party tool, an email link from a back-office system — you can hand-craft a URL that opens a specific page in the Agility portal. The same URL works whether or not the user is already signed in, and whether the tenant uses SSO or password authentication. ## The link to use ``` https:///?tenant= ``` | Environment | Portal host | | ----------- | --------------------------- | | Production | `app.agilitycredit.net` | | UAT | `app.agc-dev.com` | ## Parameters | Parameter | Required | Notes | | --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `tenant` | Yes | Your tenant ID — same value shown in the portal's **Account** settings. Used by the portal to route an unauthenticated user to the right sign-in flow, and to detect tenant mismatches for already-signed-in users. | ## Example ``` https://app.agilitycredit.net/applicants/abc123?tenant=BLWFJRNC ``` ## Flow 1. User clicks the link. 2. The Agility portal loads. 3. The portal checks the user's session and reconciles it against the `tenant` hint in the URL: - **Not signed in.** The portal redirects to the sign-in flow (SSO or password, depending on tenant config) and lands the user back on `/applicants/abc123` after authentication. - **Signed in to the same tenant.** The portal renders `/applicants/abc123` immediately. The `?tenant=` hint is silently stripped from the URL so it doesn't linger in history. - **Signed in to a different tenant, user has access to the target tenant.** The portal shows a "Switch account?" prompt; on confirm, it switches tenants and lands on `/applicants/abc123`. - **Signed in to a different tenant, user doesn't have access to the target tenant.** The portal shows a "You don't have access to this account" message and lets the user sign out. The user always ends up on the right page, or gets a clear reason why they can't. ## Common patterns **"Open in Agility" button on your dashboard:** ``` https://app.agilitycredit.net/?tenant= ``` No path — drop the user on the portal home and let them navigate from there. **Deep-link an email or notification to a specific resource:** ``` https://app.agilitycredit.net/applicants/abc123?tenant= ``` Authenticates the user (if needed) and lands them on the applicant detail page. ## Safety notes - The link works for any user in the named tenant — Agility doesn't pre-restrict who's allowed to click it. Access control on the destination page (e.g. who's allowed to view `/applicants/abc123`) is enforced once the user is signed in. - The `tenant` hint is just a hint. The user's actual access is determined by their session — Agility never grants access based on a URL parameter alone. - If the tenant doesn't exist or has been disabled, the user is redirected to the portal's `/error` page with a generic error code. Source: https://apidocs.agilitycredit.com/deep-links --- # Precise ID data sanitization :::warning Automatic data sanitization Before a Precise ID payload is sent to the credit bureau, every field is automatically sanitized to meet the bureau's formatting rules: disallowed characters are removed, values are upper-cased or reformatted where required, and anything longer than a field's maximum length is truncated. Values that cannot be made valid — for example a phone number that is not exactly 10 digits, or an enum value the bureau does not recognise — are **dropped** from the bureau request. Your **original, unaltered values are always stored** on the applicant record; only the copy forwarded to the bureau is sanitized. To avoid surprises, submit data that already conforms to the rules below. ::: ## Field sanitization rules | Field | Allowed characters | Max length | If invalid | |-------|--------------------|------------|------------| | `first_name` | letters, hyphen, space, apostrophe | 32 | other characters removed | | `last_name` | letters, hyphen, space, apostrophe | 32 | an apostrophe is kept only after `D`, `L` or `O` (e.g. `O'Brien`); other characters removed | | `middle_name` | letters only | 32 | spaces & punctuation removed | | `suffix` | `JR`, `SR`, `II`–`IX` | — | dropped if not a listed value | | `ssn` | digits, hyphen, space | 11 | letters & other characters removed; reduced to 9 digits | | `dob` | `YYYY-MM-DD` or `YYYY` | — | request rejected if malformed | | `dob_year` | digits | 4 | — | | `age` | integer 14–120 | — | — | | `phone_mobile`, `phone_home`, `phone_work` | digits only | 10 | leading `1` removed; dropped unless exactly 10 digits | | `emails[]` | letters, digits, `@` `_` `-` `.` | 90 | other characters removed; max 3 emails | | `address.street1`, `address.street2` | letters, digits, `/` `.` `-` `#`, space | 60 | other characters removed | | `address.city` | letters, apostrophe, period, hyphen, space | 38 | other characters removed | | `address.state_code` | letters (upper-cased) | 2 | digits & symbols removed | | `address.zip` | digits, hyphen | 10 | letters & symbols removed | | `employment.employer_name` | letters, digits, `#` `&` `-`, space | 23 | other characters removed | | `employment.employer_address.street1` | letters, digits, `/` `.` `-` `#`, space | 25 | other characters removed | | `employment.employer_address.city` | letters, apostrophe, period, hyphen, space | 18 | other characters removed | | `employment.income.type_of_income` | `OTHER`, `NET`, `GROSS` | — | dropped if not a listed value | | `employment.income.income_frequency` | `H` `W` `B` `M` `T` `Q` `S` `Y` | — | dropped if not a listed value | | `mothers_maiden_name` | letters, hyphen, space, apostrophe | 60 | other characters removed | | `spouse_name` | letters, hyphen, space, apostrophe | 96 | other characters removed | | `driver_license.number` | letters, digits, space, `*`, `-` | 21 | other characters removed | | `driver_license.state` | letters (upper-cased) | 2 | digits & symbols removed | `address_previous` and `employment_previous` follow the same rules as `address` and `employment` respectively. Source: https://apidocs.agilitycredit.com/data-sanitization/precise-id --- # Credit Report data sanitization :::warning Automatic data sanitization Before a Credit Report payload is sent to the credit bureau, some fields are automatically sanitized to meet the bureau's formatting rules: disallowed characters are removed, values are upper-cased or reformatted where required, and anything longer than a field's maximum length is truncated. Your **original, unaltered values are always stored** on the applicant record; only the copy forwarded to the bureau is sanitized. To avoid surprises, submit data that already conforms to the rules below. ::: ## Field sanitization rules
Field Bureau Allowed characters Max length If invalid
applicant_main.first_name, applicant_co.first_name Equifax All characters 15 Accepted as entered
Experian letters, hyphen 32 other characters removed
TransUnion All characters 20 Accepted as entered and uppercased
applicant_main.last_name, applicant_co.last_name Equifax All characters 25 Accepted as entered
Experian letters, hyphen, apostrophe 32 an apostrophe is kept only after D, L or O (e.g. O'Brien); other characters removed
TransUnion All characters 32 Accepted as entered and uppercased
applicant_main.middle_name, applicant_co.middle_name Equifax All characters 15 Accepted as entered
TransUnion All characters 20 Accepted as entered and uppercased
applicant_main.address, applicant_co.address Experian letters, numbers, space, #, period, forward slash 59 Commas removed
TransUnion All characters Accepted as entered and uppercased
Source: https://apidocs.agilitycredit.com/data-sanitization/credit-report