Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: Valjean Specialty agent Answers API
version: 1.0.0
summary: Check appetite, send submissions, answer what's needed, follow files, get quotes and ask to bind.
description: 'For retail agencies'' own software and AI assistants. Every key sees only its own agency''s files. A named Valjean broker works every file: chooses the markets, confirms anything you send, presents quotes and completes binding. Market names appear only on quotes your broker presents.'
contact:
name: Valjean Specialty
email: submissions@valjeanspecialty.com
url: https://valjeanspecialty.com
license:
name: 'Proprietary: for Valjean Specialty agencies'
url: https://valjeanspecialty.com
x-mcp:
url: https://api.valjeanspecialty.com/api/mcp
transport: streamable-http
tools:
- whoami
- check_appetite
- get_requirements
- create_submission
- list_submissions
- get_submission
- answer_questions
- underwriter_questions
- add_document
- get_upload_link
- finish_upload
- send_documents
- whats_new
- list_documents
- get_document_link
- request_bind
- list_bind_requests
- withdraw_bind_request
- ask_instant_quotes
- list_policies
- get_policy
- list_invoices
- list_quotes
servers:
- url: https://api.valjeanspecialty.com/api/agent/v1
description: Live and demo (the key decides which book you see)
security:
- bearerAuth: []
- apiKeyHeader: []
- oauth2:
- read
tags:
- name: Answers
description: Answer what's needed, once for every market.
paths:
/submissions/{id}/answers:
patch:
operationId: answerQuestions
summary: Answer what's needed, once for every market
description: 'Answers open items by key; one answer fills every market that asks the same thing. fields only fill blanks: anything Valjean already has is kept. Answers that don''t fit the item''s answer type come back in rejected and aren''t saved. Or send message: the answers in plain sentences, and Valjean maps them to the open items (from_message shows what it understood; anything sent by key wins). A broker confirms everything. POST to the same path works too.
Needs a **read-and-submit** key and an agency with a signed broker agreement.'
tags:
- Answers
x-valjean-scope: submit
security:
- bearerAuth: []
- apiKeyHeader: []
- oauth2:
- submit
parameters:
- name: id
in: path
required: true
description: Submission id.
schema:
type: string
format: uuid
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
answers:
default: []
maxItems: 50
type: array
items:
type: object
properties:
key:
type: string
pattern: ^[a-z0-9_]{1,80}$
value:
type: string
minLength: 1
maxLength: 4000
required:
- key
- value
additionalProperties: false
fields:
default: {}
type: object
properties:
annual_revenue:
type: number
minimum: 0
maximum: 100000000000
annual_payroll:
type: number
minimum: 0
maximum: 100000000000
tiv:
type: number
minimum: 0
maximum: 100000000000
years_in_business:
type: integer
minimum: 0
maximum: 300
subcontractor_costs:
type: number
minimum: 0
maximum: 100000000000
naics_code:
type: string
pattern: ^[0-9]{2,6}$
class_description:
type: string
maxLength: 300
operations_description:
type: string
maxLength: 4000
requested_limits:
type: string
maxLength: 500
prior_carrier:
type: string
maxLength: 200
expiring_premium:
type: number
minimum: 0
maximum: 100000000000
target_premium:
type: number
minimum: 0
maximum: 100000000000
additionalProperties: false
message:
type: string
minLength: 1
maxLength: 4000
additionalProperties: false
example:
answers:
- key: hours
value: 11am to midnight, 7 days
message: Seats 120. Payroll is $640K and they've been open 12 years.
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/AnswerResult'
example:
answered:
- hours
- capacity
unmatched: []
rejected: []
upload_instead: []
fields_filled:
- annual_payroll
- years_in_business
fields_kept: []
from_message:
answers:
- key: capacity
value: '120'
fields:
- annual_payroll
- years_in_business
not_understood: []
note: Answers go to your broker to confirm. Anything already on file was kept.
needs: []
headers:
RateLimit-Policy:
$ref: '#/components/headers/RateLimitPolicy'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'413':
$ref: '#/components/responses/TooLarge'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/ServerError'
'503':
$ref: '#/components/responses/Unavailable'
x-codeSamples:
- lang: shell
label: curl
source: "curl -X PATCH \"https://api.valjeanspecialty.com/api/agent/v1/submissions/SUBMISSION_ID/answers\" \\\n -H \"Authorization: Bearer $VALJEAN_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"answers\":[{\"key\":\"hours\",\"value\":\"11am to midnight, 7 days\"}],\"message\":\"Seats 120. Payroll is $640K and they've been open 12 years.\"}'"
- lang: javascript
label: Node
source: "const res = await fetch(\"https://api.valjeanspecialty.com/api/agent/v1/submissions/SUBMISSION_ID/answers\", {\n method: \"PATCH\",\n headers: { Authorization: `Bearer ${process.env.VALJEAN_KEY}`, \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n \"answers\": [\n {\n \"key\": \"hours\",\n \"value\": \"11am to midnight, 7 days\"\n }\n ],\n \"message\": \"Seats 120. Payroll is $640K and they've been open 12 years.\"\n }),\n});\nconsole.log(await res.json());"
- lang: python
label: Python
source: "import os, requests\n\nres = requests.patch(\n \"https://api.valjeanspecialty.com/api/agent/v1/submissions/SUBMISSION_ID/answers\",\n headers={\"Authorization\": f\"Bearer {os.environ['VALJEAN_KEY']}\"},\n json={\n \"answers\": [\n {\n \"key\": \"hours\",\n \"value\": \"11am to midnight, 7 days\"\n }\n ],\n \"message\": \"Seats 120. Payroll is $640K and they've been open 12 years.\"\n },\n)\nprint(res.json())"
x-agentic-access:
action: update
consequence: Records answers on the file.
human_in_the_loop: A broker confirms every answer; anything already on file is kept.
reversible: Send a corrected answer for the same key; your broker sees both.
confirm_with_user: true
post:
operationId: answerQuestionsViaPost
summary: Answer what's needed, once for every market (POST form)
description: 'Same as PATCH /submissions/{id}/answers; kept for clients that can''t send PATCH.
Answers open items by key; one answer fills every market that asks the same thing. fields only fill blanks: anything Valjean already has is kept. Answers that don''t fit the item''s answer type come back in rejected and aren''t saved. Or send message: the answers in plain sentences, and Valjean maps them to the open items (from_message shows what it understood; anything sent by key wins). A broker confirms everything. POST to the same path works too.
Needs a **read-and-submit** key and an agency with a signed broker agreement.'
tags:
- Answers
x-valjean-scope: submit
security:
- bearerAuth: []
- apiKeyHeader: []
- oauth2:
- submit
parameters:
- name: id
in: path
required: true
description: Submission id.
schema:
type: string
format: uuid
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
answers:
default: []
maxItems: 50
type: array
items:
type: object
properties:
key:
type: string
pattern: ^[a-z0-9_]{1,80}$
value:
type: string
minLength: 1
maxLength: 4000
required:
- key
- value
additionalProperties: false
fields:
default: {}
type: object
properties:
annual_revenue:
type: number
minimum: 0
maximum: 100000000000
annual_payroll:
type: number
minimum: 0
maximum: 100000000000
tiv:
type: number
minimum: 0
maximum: 100000000000
years_in_business:
type: integer
minimum: 0
maximum: 300
subcontractor_costs:
type: number
minimum: 0
maximum: 100000000000
naics_code:
type: string
pattern: ^[0-9]{2,6}$
class_description:
type: string
maxLength: 300
operations_description:
type: string
maxLength: 4000
requested_limits:
type: string
maxLength: 500
prior_carrier:
type: string
maxLength: 200
expiring_premium:
type: number
minimum: 0
maximum: 100000000000
target_premium:
type: number
minimum: 0
maximum: 100000000000
additionalProperties: false
message:
type: string
minLength: 1
maxLength: 4000
additionalProperties: false
example:
answers:
- key: hours
value: 11am to midnight, 7 days
message: Seats 120. Payroll is $640K and they've been open 12 years.
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/AnswerResult'
example:
answered:
- hours
- capacity
unmatched: []
rejected: []
upload_instead: []
fields_filled:
- annual_payroll
- years_in_business
fields_kept: []
from_message:
answers:
- key: capacity
value: '120'
fields:
- annual_payroll
- years_in_business
not_understood: []
note: Answers go to your broker to confirm. Anything already on file was kept.
needs: []
headers:
RateLimit-Policy:
$ref: '#/components/headers/RateLimitPolicy'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'413':
$ref: '#/components/responses/TooLarge'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/ServerError'
'503':
$ref: '#/components/responses/Unavailable'
x-codeSamples:
- lang: shell
label: curl
source: "curl -X POST \"https://api.valjeanspecialty.com/api/agent/v1/submissions/SUBMISSION_ID/answers\" \\\n -H \"Authorization: Bearer $VALJEAN_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"answers\":[{\"key\":\"hours\",\"value\":\"11am to midnight, 7 days\"}],\"message\":\"Seats 120. Payroll is $640K and they've been open 12 years.\"}'"
- lang: javascript
label: Node
source: "const res = await fetch(\"https://api.valjeanspecialty.com/api/agent/v1/submissions/SUBMISSION_ID/answers\", {\n method: \"POST\",\n headers: { Authorization: `Bearer ${process.env.VALJEAN_KEY}`, \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n \"answers\": [\n {\n \"key\": \"hours\",\n \"value\": \"11am to midnight, 7 days\"\n }\n ],\n \"message\": \"Seats 120. Payroll is $640K and they've been open 12 years.\"\n }),\n});\nconsole.log(await res.json());"
- lang: python
label: Python
source: "import os, requests\n\nres = requests.post(\n \"https://api.valjeanspecialty.com/api/agent/v1/submissions/SUBMISSION_ID/answers\",\n headers={\"Authorization\": f\"Bearer {os.environ['VALJEAN_KEY']}\"},\n json={\n \"answers\": [\n {\n \"key\": \"hours\",\n \"value\": \"11am to midnight, 7 days\"\n }\n ],\n \"message\": \"Seats 120. Payroll is $640K and they've been open 12 years.\"\n },\n)\nprint(res.json())"
x-agentic-access:
action: update
consequence: Records answers on the file.
human_in_the_loop: A broker confirms every answer; anything already on file is kept.
reversible: Send a corrected answer for the same key; your broker sees both.
confirm_with_user: true
/submissions/{id}/flags:
get:
operationId: getUnderwriterFlags
summary: What underwriters will ask about
description: 'Things on this file underwriters usually question (payroll not split by class, whether subcontractors are insured, gaps or stale loss runs, large losses without detail), each with a plain question to answer now through the answers endpoint. Read-only, and drawn only from your own file.
Any key (read or read-and-submit).'
tags:
- Answers
x-valjean-scope: read
security:
- bearerAuth: []
- apiKeyHeader: []
- oauth2:
- read
parameters:
- name: id
in: path
required: true
description: Submission id.
schema:
type: string
format: uuid
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/UnderwriterFlags'
example:
submission_id: 6f2a1c9e-4b3d-4e8a-9c21-7d4e5f6a8b90
flags:
- key: loss_runs_gap
severity: high
title: Loss runs cover 3 of the 5 years needed
detail: Loss runs on file cover 2023 to 2026. Markets want 5 years.
question: Can you send currently valued loss runs for the two earlier years, or confirm the insured had no coverage then?
note: Underwriters will likely ask about these. Answering them now (POST /submissions/{id}/answers) usually saves a round trip.
headers:
RateLimit-Policy:
$ref: '#/components/headers/RateLimitPolicy'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/ServerError'
x-codeSamples:
- lang: shell
label: curl
source: "curl -X GET \"https://api.valjeanspecialty.com/api/agent/v1/submissions/SUBMISSION_ID/flags\" \\\n -H \"Authorization: Bearer $VALJEAN_KEY\""
- lang: javascript
label: Node
source: "const res = await fetch(\"https://api.valjeanspecialty.com/api/agent/v1/submissions/SUBMISSION_ID/flags\", {\n method: \"GET\",\n headers: { Authorization: `Bearer ${process.env.VALJEAN_KEY}`, },\n});\nconsole.log(await res.json());"
- lang: python
label: Python
source: "import os, requests\n\nres = requests.get(\n \"https://api.valjeanspecialty.com/api/agent/v1/submissions/SUBMISSION_ID/flags\",\n headers={\"Authorization\": f\"Bearer {os.environ['VALJEAN_KEY']}\"},\n)\nprint(res.json())"
x-agentic-access:
action: read
consequence: Reads only; changes nothing.
human_in_the_loop: None needed.
reversible: Nothing to undo.
confirm_with_user: false
components:
schemas:
UnderwriterFlags:
type: object
properties:
submission_id:
type: string
format: uuid
flags:
type: array
items:
type: object
properties:
key:
type: string
description: What kind of question, e.g. lump_sum_payroll, subs_insurance_unanswered, uninsured_subs, exposure_unrealistic, loss_runs_gap, loss_runs_stale, large_loss_undocumented.
severity:
type: string
description: 'high: underwriters usually won''t quote until it''s answered.'
enum:
- high
- medium
title:
type: string
detail:
type: string
description: Why it stands out, from the facts on your file.
question:
type: string
description: The question to answer ahead of time.
required:
- key
- severity
- title
- detail
- question
note:
type: string
required:
- submission_id
- flags
- note
AnswerSpec:
type: object
properties:
type:
type: string
description: How to answer. "file" means upload a document instead.
enum:
- text
- number
- money
- percent
- yes_no
- choice
- date
- file
options:
type: array
items:
type: string
description: 'For type "choice": the allowed answers.'
required:
- type
Error:
type: object
properties:
error:
type: object
properties:
code:
type: string
description: Machine-readable code, e.g. invalid_request, read_only, not_found, closed, rate_limited.
message:
type: string
description: Plain-English explanation you can show a person.
issues:
type: array
items:
type: string
description: 'Validation problems, as "field: problem" (400 invalid_request only).'
required:
- code
- message
required:
- error
AnswerResult:
type: object
properties:
answered:
type: array
items:
type: string
description: Keys that were recorded.
unmatched:
type: array
items:
type: string
description: Keys not asked on this file (still saved in the file note).
rejected:
type: array
items:
type: object
properties:
key:
type: string
reason:
type: string
required:
- key
- reason
description: 'Answers that didn''t fit the expected type: not saved.'
upload_instead:
type: array
items:
type: string
description: Keys that need a document upload rather than text.
fields_filled:
type: array
items:
type: string
description: Fields that were blank and are now filled.
fields_kept:
type: array
items:
type: string
description: 'Fields Valjean already had: kept, not overwritten.'
from_message:
type: object
properties:
answers:
type: array
items:
type: object
properties:
key:
type: string
value:
type: string
required:
- key
- value
description: Answers read from message (keys you also sent directly are left to yours).
fields:
type: array
items:
type: string
description: Fields read from message.
not_understood:
type: array
items:
type: string
description: Sentences or values that couldn't be mapped. Send them by key, or reword.
required:
- answers
- fields
- not_understood
description: 'Only when message was sent: what Valjean understood from it.'
note:
type: string
needs:
type: array
items:
$ref: '#/components/schemas/Need'
description: What's still open after these answers.
required:
- answered
- unmatched
- rejected
- upload_instead
- fields_filled
- fields_kept
- note
- needs
Need:
type: object
properties:
key:
type: string
label:
type: string
type:
type: string
description: document, field, question, photo, or "requested" (an item your broker asked for).
level:
type: string
enum:
- required
- recommended
how:
type: string
description: '"answer", "upload a document" or "answer or upload".'
answer:
$ref: '#/components/schemas/AnswerSpec'
required:
- key
- label
- type
- level
- how
- answer
responses:
Forbidden:
description: 'Not allowed: read_only (a read key tried to submit), agency_not_active (no signed broker agreement), not_offered (instant quotes are off), forbidden (API access off for the agency). Don''t retry.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
error:
code: read_only
message: This key can read only. Ask your Valjean broker for a key that can submit.
NotFound:
description: Not found, or not your agency's (not_found). The API never says which.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
error:
code: not_found
message: Submission not found.
Unauthorized:
description: 'Key or token missing, wrong, expired or revoked (unauthorized). Don''t retry: get a new key or sign in again.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
error:
code: unauthorized
message: This API key isn't valid. Ask your Valjean broker for a new one.
ServerError:
description: Something went wrong on Valjean's side (server_error). Safe to retry later; creates are idempotent with client_reference or an Idempotency-Key header.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
error:
code: server_error
message: Something went wrong. Try again shortly.
Conflict:
description: 'The file''s state doesn''t allow it: closed, not_open, quote_expired, already_requested, not_withdrawable (the bind request already went to the market: call your broker), no_instant_market, limit, or ai_off (plain-sentence answers aren''t switched on; send answers by key). Don''t retry the same request.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
error:
code: closed
message: This submission is closed.
BadRequest:
description: Fix the request and send it again (invalid_request, invalid_json).
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
error:
code: invalid_request
message: Check the request.
issues:
- 'insured.state: Too small: expected string to have >=2 characters'
TooLarge:
description: 'Body or file too large (too_large): JSON bodies up to 64 KB, direct uploads 4 MB, upload links 25 MB.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
error:
code: too_large
message: Files over 4 MB need an upload link.
Unavailable:
description: 'A service Valjean relies on didn''t answer (ai_unavailable: the message couldn''t be read just now). Retry shortly, or send answers by key.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
error:
code: ai_unavailable
message: Couldn't read the message just now. Try again, or send answers by key.
RateLimited:
description: 'Too many requests (rate_limited): 120 a minute per key, 30 a minute per IP on public endpoints, counted across all of Valjean''s servers. Wait Retry-After seconds.'
headers:
Retry-After:
description: Seconds to wait.
schema:
type: integer
example: 60
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
error:
code: rate_limited
message: Too many requests. Try again in a minute.
headers:
RateLimitPolicy:
description: 'The rate limit for this key, as an IETF RateLimit-Policy: 120 requests (q) per 60-second window (w), counted across all of Valjean''s servers. Over it, you get 429 with Retry-After.'
schema:
type: string
example: '"per-key";q=120;w=60'
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: vjk_live_… / vjk_demo_… API key, or an OAuth access token (vjat_…)
description: 'Send the key as `Authorization: Bearer vjk_…`. Keys come from your agency portal or your Valjean broker.'
apiKeyHeader:
type: apiKey
in: header
name: X-Api-Key
description: The same API key in an X-Api-Key header, for tools that can't set Authorization.
oauth2:
type: oauth2
description: 'For AI apps that connect by signing in. Authorization code flow with PKCE (S256 only), public clients (no secret), dynamic client registration at https://api.valjeanspecialty.com/oauth/register. Discovery: https://api.valjeanspecialty.com/.well-known/oauth-authorization-server. Access tokens last 1 hour; refresh tokens 30 days and rotate on use. Tokens stop when the API key or portal link that approved them is revoked.'
flows:
authorizationCode:
authorizationUrl: https://api.valjeanspecialty.com/oauth/authorize
tokenUrl: https://api.valjeanspecialty.com/oauth/token
refreshUrl: https://api.valjeanspecialty.com/oauth/token
scopes:
read: Appetite, requirements, status, quotes, documents, policies
submit: Also send files, answers, documents, bind requests and instant-quote asks
x-event-types:
- submission.received
- submission.status_changed
- needs.requested
- markets.updated
- quote.presented
- bind.status_changed
- policy.bound
- policy.status_changed
- document.available
- invoice.sent
- invoice.paid
- invoice.overdue