Skip to main content

DeX API

Everything you need to take a business from its first answer to real carrier quotes: start an application, show the form, add locations, drivers and the other repeating parts, and collect the quotes. Your applications covers listing them for a dashboard and taking them over, and the last chapter lists the standard questions every application asks.


Before you start​

SettingDetails
Base URLThe host Bold Penguin gives you for each environment, followed by /v1: https://<api host>/v1. Every path in these docs is relative to it.
VersionThis is version 1. Within it, fields, endpoints and events are only ever added, never removed or changed, so ignore what you don't recognise. Every response has an API-Version header (for example 1.0). See Versions.
FormatJSON request and response bodies. Send Content-Type: application/json.
OpenAPI specOpenAPI 3.1.0 (YAML) · OpenAPI 3.0.3 compatible (YAML)
AuthenticationAuthorization: Bearer <token> on every call except GET /health, GET /storefronts/{code}, POST /auth/token and POST /guest. If Bold Penguin gave you a client id and secret, get a token from POST /auth/token, for your client or, with an agent's username and password or SSO, for that agent; otherwise you get one when you start a guest application.
StorefrontYour storefront code and tenant id, from Bold Penguin. If captcha is on for your storefront, you also need a reCAPTCHA token from the browser.
IDsEvery id is a UUID. A malformed id is rejected with 400 before anything else happens.

Build your app​

This chapter walks through building a web app that takes a business from its first answer to carrier quotes, in two versions:

Both use the same form renderer and the same quote pages. Part 1 builds them; Part 2 reuses them and adds sign-in, the dashboard and taking applications over. The examples use fetch and plain JavaScript, so they carry over to any framework.

// Used by every example below.
const API = 'https://<api host>/v1';

async function api(path, { token, method = 'GET', body } = {}) {
const res = await fetch(`${API}${path}`, {
method,
headers: {
...(token ? { Authorization: `Bearer ${token}` } : {}),
...(body !== undefined ? { 'Content-Type': 'application/json' } : {}),
},
body: body !== undefined ? JSON.stringify(body) : undefined,
});
const json = res.status === 204 ? null : await res.json().catch(() => null);
if (!res.ok) throw Object.assign(new Error(`${method} ${path}: ${res.status}`), { status: res.status, body: json });
return json;
}

Part 1: A guest application on your storefront​

What you need from Bold Penguin: your storefront code, your tenant id, and, if captcha is on for your storefront, your reCAPTCHA site key.

Landing page   GET  /storefronts/{code}              branding, captcha setting
Start POST /guest -> { token_id, token }
GET /tokens/{token_id} -> the application
Questions POST /application_forms/{id} a save per answer, as the customer gives it
POST /location, /driver, … more locations, drivers, … as pages ask
Quotes WebSocket /application_forms/{id}/events (or poll GET …/quote_requests)
Buy POST …/quote_requests/{quote_request_id}/select, then …/bind -> retrieve_url

Step 1. Load your storefront​

Before the customer does anything, load your storefront's settings. No token is needed:

const storefront = await api(`/storefronts/${STOREFRONT_CODE}`);

Use it to brand the landing page and to know how to start the application:

FieldUse it for
tenant_idThe tenant to start the application in (step 2)
enable_captchatrue: get a reCAPTCHA token before starting (step 2)
agency_name, logo_url, primary_hex_color, accent_hex_colorYour header and colours
phone, email, address, hours_by_day, timezone"Talk to an agent" contact details
landing_page_headline, landing_page_subheadlineThe landing page's text
show_quotesWhether customers see prices on this storefront. When it's false, show a "we'll be in touch" confirmation at the end instead of the quotes page.
footer_disclaimer_html, faq_htmlYour footer and FAQ (already sanitized)

A 404 means the code is wrong or the storefront isn't active.

Step 2. Start the application​

Ask for a first answer or two on your landing page (the business name, the state, what it does), then start the application with them:

const recaptcha_token = storefront.enable_captcha
? await grecaptcha.execute(RECAPTCHA_SITE_KEY, { action: 'start' })
: undefined;

const { token_id, token } = await api('/guest', {
method: 'POST',
body: {
tenant_id: storefront.tenant_id,
source: 'storefront',
source_metadata: { storefront_code: STOREFRONT_CODE, ...(recaptcha_token ? { recaptcha_token } : {}) },
application_form: {
field_values: [{ code: 'mqs_business_name', value: 'Acme Carpentry LLC' }],
},
},
});

grecaptcha.execute is reCAPTCHA v3. With a v2 (checkbox) site key, send the widget's grecaptcha.getResponse() instead. Either way the token works once, so get a new one if starting fails.

token is the customer's Bearer token for every call from here on. A 403 means the storefront code is wrong, the captcha failed (get a new token and try again), or the storefront doesn't take new applications.

Then load the application, once, with token_id:

const { application_form: form } = await api(`/tokens/${token_id}`, { token });
sessionStorage.setItem('application', JSON.stringify({ id: form.id, token }));

Keep form.id and token. token_id works only once; from now on load the application by its id. If the customer reloads the page, pick up where they were:

const saved = JSON.parse(sessionStorage.getItem('application') ?? 'null');
const form = saved && await api(`/application_forms/${saved.id}`, { token: saved.token });

Step 3. Follow the application live​

Open the application's live updates now, so new pages, quotes and messages reach the customer without polling:

const ws = new WebSocket(`${API.replace(/^http/, 'ws')}/application_forms/${form.id}/events?token=${token}`);
ws.onmessage = (e) => {
const { event, data } = JSON.parse(e.data);
if (event === 'ready' || event === 'form.updated') render(data.application_form);
if (event === 'quote_request.updated') renderQuote(data.quote_request);
if (event === 'quote_request.removed') removeQuote(data.quote_request_id);
if (event === 'form.taken_over') showTakenOver(data); // an agent took the application over
};
ws.onclose = (e) => { if (e.code !== 4401 && e.code !== 4403 && e.code !== 4404) setTimeout(connect, 3000); };

Wrap it in a connect() function so it can reconnect: ready sends the whole application again. When an agent takes the application over, form.taken_over arrives: the agent carries on with a copy, so stop taking answers and tell the customer an agent will be in touch. The events are listed in Live updates.

Step 4. Render the page to answer​

The application is a list of pages, like a wizard. Show the first page that's open and not finished yet:

function currentPage(form) {
return form.pages.find((p) => p.active && !p.valid) ?? form.pages.at(-1);
}

Show every page's title in a progress bar: done (valid), current, and upcoming (not active yet). Draw the current page from the top down:

  1. The form's messages with location: null, as a banner, and the page's own messages at the top of the page.
  2. Each row by its layout (Rows): one field, fields side by side, an address block, or a repeat row of locations, drivers and the like.
  3. Each field by its type (Fields), filled in with its value, with its label, help, placeholder, required marker and messages.

Two kinds of field need a call of their own:

  • search (the business type): look up choices as the customer types, with POST /choices, and send the chosen choice's alias.
  • Repeat rows (another location, another driver): the add_label button, when add_label isn't null, adds an item with POST /location, POST /driver, …, or an empty one with POST /application_forms/{id}/{section}. Each entry's remove button calls DELETE /application_forms/{id}/{section}/{item_id}. Render from the application each call returns.

Check each field's validation as the customer types, and show errors once they've left a field or pressed Next (Validity and errors).

Step 5. Save each answer​

Save each answer as soon as the customer gives it, not when they press Next. An answer can add questions to the page they're on (say "Do you currently have insurance?" is "Yes": the current carrier's questions appear right under it), and the save's response is what has them. Saving answer by answer shows them while the customer is still on the page.

  • Choices (select, multiselect, radio, checkbox) and dates: save on change.
  • Text and numbers: save when the customer leaves the field, or after a short pause in typing (half a second or so), not on every keystroke.
  • One save at a time. If the customer changes more while a save is out, send those in the next save, with the revision from the response you just got. Saves sent at the same time can finish in either order.

Every save sends autocomplete: true and the revision of the application on screen:

let saving = Promise.resolve();
const queued = new Map(); // field id -> the field with its new value

function saveAnswer(field) {
queued.set(field.id, field);
saving = saving.then(async () => {
if (!queued.size) return;
const changed = [...queued.values()];
queued.clear();
const saved = await api(`/application_forms/${form.id}`, {
token,
method: 'POST',
body: {
application_form: {
// One entry per changed field; a field inside a location also sends its location_id.
field_values: changed.map((f) => ({ code: f.code, value: f.value, ...itemIdsOf(f) })),
autocomplete: true,
},
revision: form.revision,
},
});
form = saved;
render(saved);
if (saved.changes?.has_changes) toast(saved.changes.message);
});
return saving;
}

itemIdsOf(f) is the field's item id, when it has one: every *_id key on the field (location_id, building_id, …), sent back as it came.

Render from each response, which is the whole application again:

  • New questions on this page (changes.fields_added) appear where the form puts them. Highlight them. Keep what the customer is typing in a field you haven't saved yet; don't overwrite it with the response's value.
  • A 422 means an answer was rejected. Show error.details.errors on the fields, or error.message.
  • Next moves on once the page is valid, after any save still out has finished: to the next page that's active and not valid. Saving is what adds pages: completing the Eligibility pages adds the locations, vehicles and carrier pages the answers call for. Don't keep your own list of pages; take it from each response.

Step 6. Show the quotes​

Each carrier is asked for a quote as soon as its page is complete, so quotes start arriving while the customer is still answering. When every page is valid and the application's state is completed or quoted, move to your quotes page. If show_quotes is false for your storefront, show your confirmation page instead and stop here.

Render the application's quote_requests, and update them from the live updates (or poll GET /application_forms/{id}/quote_requests every few seconds):

request_statusShow
unsent, sent, in_progressThe carrier's logo with a spinner
completedA quote card: carrier.name and carrier.logo_url, annual_price (and monthly_price), labels, and a Buy button when bindable
referral"The carrier needs to review your application"
failed, timed_outLeave it out, or say the carrier couldn't quote

Quoting is finished when none is unsent, sent or in_progress (the application's state is then quoted). Also show the form's messages with location: "quote_presentation" on this page. See Collect the quotes for suppliers' options, labels and calls to action.

The proposal. A completed quote's proposal, once it's there, is the carrier's PDF. Its url needs the Bearer token, so download it with fetch rather than a plain link:

const res = await fetch(`${API}${quote.proposal.url}?disposition=inline`, { headers: { Authorization: `Bearer ${token}` } });
window.open(URL.createObjectURL(await res.blob()));

Step 7. Buy​

When the customer picks a quote, record their choice and bind it:

await api(`/application_forms/${form.id}/quote_requests/${quote.id}/select`, { token, method: 'POST' });
const { retrieve_url } = await api(`/application_forms/${form.id}/quote_requests/${quote.id}/bind`, { token, method: 'POST', body: {} });
if (retrieve_url) window.open(retrieve_url, '_blank');

The customer finishes buying on the carrier's site. If the quote has calls_to_action, its first one replaces the Buy button (Calls to action). When retrieve_url is null, keep showing the quote until its request_status is bound or binding_failed. Checkout inside your app isn't offered yet: every quote has online_bind: false for now.

Checklist for Part 1

  • The landing page loads the storefront, and gets a captcha token when it's on
  • Starting saves the application's id and token, so a reload carries on
  • Live updates reconnect after a drop
  • Every row layout and field type renders, including repeat rows and search fields
  • Each answer is saved as it's given, one save at a time, with autocomplete: true and revision, and the page re-renders from the response, new questions included
  • Messages show as a banner, at the top of the page and with their question
  • Quotes update as they arrive, with the proposal download and Buy

Part 2: An agent application with sign-in​

Here the user is one of your agents, quoting for their customers. What you need from Bold Penguin: your client id and client secret, and a Bold Penguin login for each agent.

Sign in        POST /auth/token  grant_type=password  -> { access_token, refresh_token, expires_in }
Dashboard GET /application_forms, /application_forms/counts
New POST /application_forms -> the new application
Open GET /application_forms/{id} (take it over first when it isn't theirs)
Questions the same as Part 1, steps 3 to 5
Quotes the same as Part 1, steps 6 and 7, plus checkout in your app

Step 1. Put a small server behind your app​

An agent's sign-in gives a refresh token, and refreshing needs your client secret. Neither can go to the browser, so put a small server of your own (a backend for your app) in front of the sign-in calls:

  • POST /login on your server takes the agent's username and password, calls POST /auth/token, keeps the refresh token in the agent's server-side session, and returns only the access token and when it expires.
  • POST /refresh on your server uses the session's refresh token to get a new access token.
  • POST /logout drops the session.

The browser then calls the API directly with the access token, exactly as in Part 1.

Step 2. Sign the agent in​

Your sign-in page posts the username and password to your server, which signs in:

// On your server
const signIn = await fetch(`${API}/auth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ grant_type: 'password', username, password, client_id: CLIENT_ID, client_secret: CLIENT_SECRET }),
});
if (signIn.status === 401) return reply.status(401).send({ error: 'Wrong email or password' });
const { access_token, refresh_token, expires_in, tenant_id } = await signIn.json();
session.refresh_token = refresh_token;
return { access_token, expires_at: Date.now() + expires_in * 1000 };
  • username is the agent's Bold Penguin username, for most agents their email.
  • A 401 with error.code invalid_grant is a wrong username or password. Show one message for both.
  • Send the password straight on over HTTPS, and don't store or log it.
  • If your agents sign in to Bold Penguin through their company's identity provider, use SSO instead of a password; the rest of Part 2 is the same.

Show who's signed in with GET /user, using the access token: it returns the agent's name, email, tenant and permitted_to_bind (whether they can buy policies). See GET /user.

Step 3. Keep the token fresh​

The access token lasts expires_in seconds, from the sign-in response. Refresh it a little before expires_at, and on a 401, refresh once and retry the call:

// On your server: POST /refresh
const res = await fetch(`${API}/auth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ grant_type: 'refresh_token', refresh_token: session.refresh_token, client_id: CLIENT_ID, client_secret: CLIENT_SECRET }),
});
if (!res.ok) return reply.status(401).send(); // send the agent back to sign-in
const next = await res.json();
session.refresh_token = next.refresh_token; // each refresh token works once
return { access_token: next.access_token, expires_at: Date.now() + next.expires_in * 1000 };

When a refresh fails, send the agent to the sign-in page. Reopen the live updates socket with the new token when it changes.

Step 4. Build the dashboard​

List the agent's applications, most recently updated first, with the headline counts:

const [list, counts] = await Promise.all([
api('/application_forms?state=all&sort=updated_at&per_page=20&page=1', { token }),
api('/application_forms/counts', { token }),
]);

Each row in list.application_forms has what a table needs (Your applications):

ColumnFrom
Businesssummary.business_name, summary.business_type
Contactsummary.first_name, summary.last_name, summary.email, summary.phone
Coveragesummary.coverage_types, summary.coverage_start_date
Statusstate, percent_completed, and bind.status as a badge
Quotesquote_requests: how many are completed, and the lowest annual_price
Ownerowner.first_name, owner.last_name, with a "Not yours" badge when not_owner is true

Send state=all: without a state, the list leaves out applications that were just started (incomplete), including the ones the agent creates in step 5. Add a search box (q), filters (state, owner=current_user, labels=has_bound_policy) and paging (there are more pages while page × per_page < total). The list can lag a moment behind the latest save, so after saving, show the application from the save's response rather than reloading the list.

Step 5. Start a new application​

The New application button starts one owned by the agent. Send any answers you already have:

const form = await api('/application_forms', {
token,
method: 'POST',
body: {
application_form: {
field_values: [{ code: 'mqs_business_name', value: 'Acme Carpentry LLC' }],
autocomplete: true,
},
},
});

The response (201) is the new application, in the usual shape. There's no token id to load: go straight to Part 1, step 3 with form and the agent's access token.

Step 6. Open an existing application​

Clicking a dashboard row loads the application:

const form = await api(`/application_forms/${id}`, { token });

When form.not_owner is false, it's the agent's: carry on from Part 1, step 3.

When form.not_owner is true, a customer or another agent owns it. Show it read-only, with a Take over button, since changing it, and buying its quotes, needs it to be the agent's:

const { application_form_id, application_form } = await api(`/application_forms/${id}/take_over`, { token, method: 'POST' });
// Carry on with the copy, under its new id.
history.replaceState(null, '', `/applications/${application_form_id}`);
render(application_form);

Taking over makes a copy owned by the agent, with the same answers; see Take over an application. On the live updates, form.taken_over means someone else took over the application the agent has open: stop editing it, and open new_application_form_id if taken_over_by.id is the agent's own id (id from GET /user).

Step 7. Answer, quote and buy​

From here the agent's app works like the customer's: follow the application live, render the current page, save it, and show the quotes, as in Part 1, steps 3 to 7, with the agent's access token. The differences:

  • Quotes on someone else's application have not_owner: true and can't be bought. Show Take over to buy in place of Buy (Someone else's quotes).
  • Checkout in your app isn't offered yet: every quote has online_bind: false for now, so bind every quote as in Part 1. When it's turned on, quotes with online_bind: true can be bought and paid for without leaving your app, with the steps in Checkout in your app.
  • Calls to action can send a quote to the carrier's underwriters instead of binding it (send_to_underwriting); see Calls to action.
  • There's no show_quotes setting: agents always see the prices.

Step 8. Sign out​

Close the live updates socket, forget the access token in the browser, and call your server's POST /logout to drop the refresh token.

Checklist for Part 2

  • The client secret and refresh tokens stay on your server
  • A wrong password shows one message; the password is never stored or logged
  • Tokens refresh before they expire and after a 401, and a failed refresh signs the agent out
  • The dashboard searches, filters and pages, and flags applications that aren't the agent's
  • New application opens the new application straight away
  • Opening someone else's application offers Take over, then carries on with the copy
  • Quotes offer Buy for the agent's own quotes and Take over to buy for someone else's

How to get a quote​

There is no "get a quote" call. Bold Penguin quotes each carrier by itself, as soon as that carrier's page of the application is complete. Your job is to get every page answered and saved with autocomplete: true, then watch the application's quote_requests fill in.

1. POST /guest                          start the application -> { token_id, token }
2. GET /tokens/{token_id} load it -> the form, as pages to show
3. POST /application_forms/{id} save a page's answers with autocomplete: true
... repeat for each page; add locations, drivers, ... as the pages ask for them
4. GET /application_forms/{id}/quote_requests
poll until the quotes and proposals are in
(or follow them with live updates)
5. POST /application_forms/{id}/quote_requests/{quote_request_id}/select
mark the quote the user picks (optional)
6. POST /application_forms/{id}/quote_requests/{quote_request_id}/bind
buy it

1. Start the application​

{
"tenant_id": "<your tenant id>",
"source": "storefront",
"source_metadata": { "storefront_code": "<your storefront code>" }
}

The response is 201 with { "token_id": "…", "token": "…" }. Send token as the Bearer token on every call that follows. If you already know some answers (the business name, the state), you can send them now as application_form.field_values in the same format as a save.

2. Load the form​

{
"id": "…",
"application_form": {
"id": "<form id>",
"state": "incomplete",
"cable_token": "…",
"pages": [ … ],
"quote_requests": []
}
}

Keep application_form.id: every other call uses it. Each token id works only once; the next one is in cable_token. You won't normally need it again, because from here on you load the form by its id.

3. Answer the pages​

Show the first page (see Displaying the form), then save each answer as the user gives it (see Saving):

{
"application_form": {
"field_values": [
{ "code": "mqs_business_name", "value": "Acme Carpentry LLC" },
{ "code": "CoverageTypes", "value": ["CGL"] }
],
"autocomplete": true
}
}

The response is the whole updated form. Render from it: it has the saved values, any errors, each page's valid, and any pages or questions the answers turned on.

Always send autocomplete: true. It tells Bold Penguin to mark every page whose required answers are all in as complete, and completing pages is what moves the application forward:

  • Completing the Eligibility pages (the first page set, see Standard questions) adds the next pages, depending on the answers: locations and buildings, vehicles and drivers, and the carriers' own pages.
  • Completing a carrier's page sends that carrier a quote request.

A page completes only when every required question on it, and on every page before it, is answered. Use each page's valid to know when to let the user move on.

When a page asks for more than one of something (another location, a driver, a building), add it with POST /location, POST /driver, and so on. See Creating sections.

4. Collect the quotes​

Quotes arrive in the background. Follow them with live updates, or every few seconds, load the quote requests:

{
"application_form_id": "<form id>",
"state": "quoted",
"quote_requests": [
{
"id": "…",
"request_status": "completed",
"carrier": { "name": "…", "logo_url": "https://…", "underwriter": { "full_name": "…", "naic_number": "…", … } },
"annual_price": 1234.0,
"monthly_price": 112.5,
"quote_number": "…",
"effective_date": "2025-07-01",
"labels": ["ready_to_bind", "taxes_and_fees_included"],
"products": [{ "id": "…", "name": "General Liability", "coverage": { … } }],
"calls_to_action": [{ "trigger_type": "programmatic_bind", "action_type": "programmatic_bind_workflow", "button_text": "Buy now", "content": null }],
"bindable": true,
"rate_type": "ready_to_bind",
"online_bind": false,
"proposal": { "id": "…", "filename": "Quote_123.pdf", "attachment_type": "quote_pdf", "content_type": "application/pdf", "url": "/attachments/…" },
"retrieve_url": "https://…"
}
],
"cable_token": "…"
}

This shows the main fields; every quote request has all the fields in the quote request object.

request_statusMeaning
unsent, sent, in_progressThe carrier is still working on it. Keep polling.
completedA quote. The price is in annual_price (and monthly_price when the carrier offers monthly payment).
referralThe carrier needs to review the application before it can quote.
failedThe carrier declined or couldn't quote. translated_messages says why, when the carrier gave a reason.
timed_outThe carrier didn't answer in time (about two minutes).
bindingThe policy is being bought.
pending_paymentThe purchase is waiting for payment (for example, a payment link was sent).
binding_failedThe carrier couldn't complete the purchase.
boundThe policy was bought.

Stop polling when no quote request is unsent, sent or in_progress, and the completed quotes you want to show have their proposal. The form's state is then quoted.

stateMeaning
incompleteJust started
in_progressThe user has answered something
completedEvery page is complete; quote requests are out
quotedEvery quote request has finished
binding, boundA policy is being bought, or was bought

The same quote_requests list is also in the form (GET /application_forms/{id}), and one quote request can be refreshed on its own with GET /application_forms/{id}/quote_requests/{quote_request_id}.

products lists the products (coverages) the quote request is for, each with its name and coverage details.

Suppliers. Some carriers quote through several markets (suppliers) at once. Then options has one entry per supplier with just what differs: id, supplier (code, name, logos), quote_number, annual_price, semiannual_price, price_details, retrieve_url and products. Everything else is the quote request's. You can show each option as its own quote card. For a single quote options is [], and supplier names its supplier if it has one.

The proposal. proposal is the carrier's quote document, a PDF, once the carrier has sent it; null until then (and for carriers that don't send one). It sometimes arrives a little after the price, so keep polling a completed quote until proposal is set. Download it with its url, sending your Bearer token:

GET /attachments/{id}                      the file, to save
GET /attachments/{id}?disposition=inline the file, to show in the browser

attachments lists every document for the quote in the same form: the proposal, and sometimes a specimen policy or premium finance agreement. Once the policy is bought, it has the policy documents instead.

To buy a quote, bind it when the user picks one with bindable: true (you can select it first to record their choice):

{}
{ "quote_request_id": "…", "retrieve_url": "https://…" }

Send the user to retrieve_url (a new tab works best) to finish buying on the carrier's site. The response also has quote_request, the quote after binding.

  • Only quotes with bindable: true can be bought. bindable means the user can press Buy now: the carrier has a purchase link for this quote and the application is theirs (it's false when someone else owns it). For guests it also depends on your storefront's settings. A quote that isn't bindable can only be shown.
  • Someone else's quote has not_owner: true, and can't be bought until the user takes the application over. See someone else's quotes.
  • Is the price final? rate_type says: ready_to_bind means the quote can be bought at this price; indicative means it's an estimate and the carrier may ask more questions or review the application first; null means the carrier didn't say, so don't present the price as final. bindable doesn't tell you this.
  • When the quote has a call to action, its first calls_to_action entry is the button to show instead of the usual one. See calls to action.
  • When retrieve_url is null, the purchase continues inside Bold Penguin: keep loading the quote request until its request_status is bound (or binding_failed). The policy documents then appear in its attachments.

Checkout in your app. Checkout inside your app isn't offered yet: every quote has online_bind: false for now, so bind every quote as above. When it's turned on, a quote with online_bind: true (a call to action with trigger_type: programmatic_bind and action_type: programmatic_bind_workflow, and nothing else) can be bought and paid for without leaving your app, with the steps in Checkout in your app; only the quote's owner can check out.

Select a quote​

When the user chooses a quote, mark it as their pick, as Bold Penguin's own quote pages do:

POST /application_forms/{id}/quote_requests/{quote_request_id}/select
Authorization: Bearer <token>

200 with { quote_request_id, quote_request }, the quote read again. Bold Penguin records the quote's carrier as the user's choice, and on a guest application the quote's focused becomes true. selected is set later, when the quote is bought (bind, or starting checkout in your app). Only the quote's owner can select it: 403 otherwise.

Someone else's quotes​

A quote belongs to the user who owns the application. When it isn't the user's, it has not_owner: true: show it, but it can't be bought. Its bind URL and bind options are null: retrieve_url (and each option's), calls_to_action, actions, online_bind, plans and payment_methods. bindable is false, and bind, select and every step of checkout in your app answer 403 without changing anything.

Ask the user to take the application over to finish buying, whichever way the quote would be bought: on the carrier's site, on Bold Penguin's bind page, or with checkout in your app. Show a "Take over to buy" button in place of Buy, and when it's pressed:

  1. Take the application over. This makes a copy owned by the user.
  2. Carry on with the copy. Its quotes are the user's, with their bind URL and bind options. Pinned quotes are copied over; the others are requested again once the copy's pages are complete, so wait for them as usual. If a page has valid: false, the user finishes it first.
  3. Bind the quote there. (Checkout in your app isn't offered yet: every quote has online_bind: false for now.)

not_owner is null when Bold Penguin couldn't confirm who the user is. The bind fields are null then too, and bind, select and the checkout steps answer 503: try again.

If the user changes an answer on a page that was already complete, that page opens up again and the carriers' quotes that depended on it are withdrawn. Save again with autocomplete: true and they are re-quoted.

Which pages and carriers you get depends on your storefront. Bold Penguin decides which carriers can quote from the business's state, what it does (Naics) and the coverages it wants (CoverageTypes), among the carriers your storefront works with. The form always shows exactly what is needed: when there are no more pages to answer, you're done.

Quote labels​

labels is a list of strings describing the quote:

LabelMeaningWhat to show
ready_to_bindThe quote can be bought as quoted.“Bindable quote”
rate_indicationAn estimate, not a final price: the carrier may ask more questions or review the application before the quote can be bought.“Rate indication”, with a note that the carrier may need more information
taxes_and_fees_includedThe price already includes taxes and fees.“Includes taxes and fees” next to the price
is_additional_coverageAn add-on coverage offered alongside the main quotes (for example, extra coverage offered with a business owner's policy), not an alternative to them.List it after the main quotes, and leave it out of price comparisons
underlying_ready_to_bindThe quote builds on an underlying policy, and that underlying policy's quote is ready to be bought.Nothing needed; Bold Penguin's own apps don't show it
  • A quote can have more than one label, for example ["ready_to_bind", "taxes_and_fees_included"]. If a quote has both rate_indication and ready_to_bind, treat it as a rate indication.
  • [] means no label. Labels are only filled in when they're turned on for your account; ask your Bold Penguin integration lead. Until then every quote has [].
  • Labels matter on completed quotes. Ignore any label you don't recognise: new ones can be added.
  • rate_type is worked out from these labels for you: indicative, ready_to_bind, or null.

Calls to action​

calls_to_action is what the quote's button should do when buying it isn't the usual bind. Bold Penguin sets these up per carrier for your storefront, and a quote lists only the ones that apply to it right now. Most quotes have []: use the usual bind.

{ "trigger_type": "not_permitted_to_bind", "action_type": "dialog", "button_text": "Contact us to buy", "content": "Call 555-0100 to finish buying this policy." }

Show the first entry as the quote's button, labelled with button_text. What it does depends on action_type:

action_typeWhat to do
programmatic_bind_workflowBind the quote as usual. Checkout inside your app isn't offered yet (online_bind is false for now); with trigger_type: bridge_to_programmatic_bind, bind returns your storefront's bind page as retrieve_url: open it.
send_to_underwritingBind with { "action_type": "send_to_underwriting" } to send the application to the carrier's underwriters for review instead of buying.
dialogShow content in a message. There's nothing to bind.
wholesale_workflow, north_peak_workflow, data_collection_workflowPlacement through your wholesaler or Bold Penguin's placement desk. Not available through this API; show button_text as a note.
pin_quoteNot used through this API; ignore it.

trigger_type says why it's there:

trigger_typeIt's there when
programmatic_bindThe owner can buy policies (an agent, or your storefront's customer when the storefront lets them buy), the carrier is set up for checkout in your app, the quote is ready_to_bind, and any excess and surplus agreement is signed
bridge_to_programmatic_bindThe same, for storefronts that finish checkout on Bold Penguin's bind page; bind returns that page as retrieve_url
not_permitted_to_bindThe owner can't buy from this carrier here, and the quote hasn't gone to a wholesaler
referralThe quote is a referral: the carrier has to review the application first
sent_to_wholesalerThe quote has already been sent to your wholesaler
send_to_north_peakYour storefront has a wholesaler for placement reviews
excess_and_surplusAn excess and surplus lines quote whose agreement isn't signed yet

Ignore any action_type or trigger_type you don't recognise.

The quote request object​

A quote request is the same object everywhere: in the form's quote_requests, from the quote request endpoints, in bind's quote_request, in each row of your applications list, and in quote_request.updated events. It always has every field below, in this order. A field with no value is null, and a list with nothing in it is [], except that someone else's quote has null bind options (someone else's quotes). Ignore fields you don't recognise: new ones can be added after these.

FieldMeaning
idThe quote request's id
application_form_idThe application it belongs to
request_statusWhere the carrier is with it; see the table above
activefalse once it's been withdrawn (an answer it depended on changed)
declinedThe carrier declined to quote
expiredThe quote is past its expiry date
staleThe application changed after the quote came back, so the price may no longer hold
quote_numberThe carrier's quote number
account_numberThe carrier's account number, when it gives one
effective_dateWhen the policy would start
started_at, finished_atWhen the carrier was asked, and when it answered
created_at, updated_atTimestamps
owner_id, tenant_idThe user and tenant it belongs to
not_ownertrue when the quote isn't the user's: take the application over to buy it. null when that couldn't be checked. See someone else's quotes.
carrierThe carrier: id, code, name, logo_url, logo_url_small, labels, appointment_type, bind_payment_strategy, and underwriter (below)
supplierFor a single quote from a supplier, { code, name, logo_url, logo_url_full }; see Suppliers in Collect the quotes
productsThe products (coverages) it's for: id, name, choice_id, from_name, coverage and timestamps
optionsThe suppliers' quotes, when the carrier gave more than one; see Suppliers in Collect the quotes
annual_price, monthly_price, semiannual_priceThe premium per term. monthly_price only when the carrier offers monthly payment; semiannual_price only for six-month terms.
downpayment, number_of_installmentsFor installment payment: the first payment, and how many follow
total_installment_premiumFor integrated bind quotes, the total paid over all installments; otherwise null
surchargeAny surcharge the carrier added
price_detailsThe price broken down: new_total_premium, and premiums, taxes and fees as { description, amount } lists, plus disclaimer_html
plansThe payment plans the carrier offers, for integrated bind; null when the quote isn't yours (someone else's quotes)
selected_plan_idThe plan chosen during checkout
payment_methodsFor carriers paid online through Bold Penguin, the ways to pay; otherwise null, including when the quote isn't yours
labelsWhat kind of quote it is; see quote labels
rate_typeready_to_bind, indicative or null: whether the price is final; see To buy a quote in Collect the quotes
exclusionsWhat the carrier excluded from cover, as the carrier worded it
translated_messagesThe carrier's messages, for example why it declined, in Bold Penguin's wording
bindableThis user can press Buy now; see To buy a quote in Collect the quotes. false when the quote isn't yours.
online_bindWhether it can be bought and paid for inside your app. false on every quote for now: checkout inside your app isn't offered yet. null when the quote isn't yours (someone else's quotes)
onlineThe carrier takes purchases online
permitted_to_bindThe application's owner is allowed to buy policies
calls_to_actionWhat the quote's button should do; see calls to action; null when the quote isn't yours (someone else's quotes)
actionsFor carriers that bind in steps, the next step's choices; null when the quote isn't yours (someone else's quotes)
retrieve_urlThe carrier's page for this quote; null when the quote isn't yours (someone else's quotes)
prohibited_operationsBusiness activities the carrier won't cover, as the carrier worded them
customizableIts coverage limits can be changed by answering its product's questions again
order, scoreBold Penguin's sort order and ranking score for the quote
recommended, preferredBold Penguin recommends it; the carrier is one of your storefront's preferred carriers
selected, consumer_selected, focused, pinnedIt's the quote being bought (set by bind or checkout in your app); the customer chose this carrier; the user picked it with select (guest applications); it's pinned to the top
satisfies_application_form_product_requestIt covers every product the application asked for
proposalThe carrier's quote document; see The proposal in Collect the quotes
attachmentsEvery document for the quote; see The proposal in Collect the quotes
policyOnce bound, the policy: policy_number, effective_date, risk_company, policy_details and the plan paid with

carrier.underwriter is the insurance company that writes the policy: { full_name, title, naic_number, phone_number, email, street1, street2, city, state, zipcode } (any of them can be null), or null when the carrier has none on file. Show it where a policy's legal paper is named, for example next to the price or on a confirmation page.


Displaying the form​

Every call that returns the application returns it the same way, so one renderer covers the whole flow. The form is pages, made of rows, made of fields. A renderer only needs to switch on each row's layout and each field's type.

{
"id": "<form id>",
"state": "in_progress",
"reference_id": "LB5-4NX-CNK",
"pages": [
{
"id": "…",
"title": "General information",
"active": true,
"valid": false,
"warning": null,
"messages": [],
"rows": [
{ "layout": "inline", "label": null, "fields": [ { "code": "mqs_first_name", … }, { "code": "mqs_last_name", … } ] },
{ "layout": "stack", "label": null, "fields": [ { "code": "mqs_business_name", … } ] },
{ "layout": "address", "label": "Primary business address", "fields": [ … ] }
]
}
],
"quote_requests": [],
"messages": [],
"cable_token": "…",
"revision": "r1.…"
}

revision identifies what's on the pages; send it with your next save to learn which questions it added (see New questions after a save).

Pages​

Show pages in order, one at a time, like a wizard.

FieldUse it for
titleThe page heading, and its name in your progress bar
activeWhether the user can fill it in yet. A page becomes active once every page before it is complete. Show inactive pages as upcoming, not editable.
validWhether every field on the page passes. Enable Next only when it's true.
warningnull, or a message to show next to the title when an item on the page has questions left on an earlier page. See below.
messagesMessages to show at the top of the page. Usually [].
rowsWhat to draw, top to bottom

New pages appear in the response to the save that unlocked them. Re-render the page list after every save; don't cache it.

Adding an item can add questions to earlier pages: a location added from the Buildings page also has its questions on the Locations page. A page that shows an item with questions left to answer on an earlier page has a warning:

"warning": {
"message": "Finish answering the questions on Locations, then continue.",
"pages": [{ "id": "…", "title": "Locations" }],
"items": [{ "section": "locations", "location_id": "…", "label": "New Location" }]
}

Show message next to the page's title, and link to each page in pages. items are the unfinished items on this page. Until their earlier questions are answered, the page leaves out those items' read-only fields, which show values copied from them (a location's address on the Buildings page). They come back once those questions are answered.

Rows​

layoutDraw
stackOne field on its own line
inlineThe row's fields side by side (first and last name, phone and email)
addressAn address block: street, suite, city, state, ZIP. Use an address autocomplete if you have one.
vehicle, driver, coverage, accordion, auto_collapsed_accordion, accumulate_percentage, stepperThe row's fields as one component of that kind. Drawing them as a plain group of fields also works.
repeatA section the user can add to: locations, buildings, drivers, … See Repeat rows.

label is a heading to show above the row, or null. Only what the user should see is included: questions switched off by earlier answers, and ones Bold Penguin fills itself, are left out. So are an item's remove button (it's the entry's remove_label, see Repeat rows) and questions that only hold data for the form.

Fields​

{
"id": "<answer id>",
"code": "mqs_state",
"type": "select",
"format": null,
"label": "State",
"help": null,
"placeholder": null,
"required": true,
"readonly": false,
"value": "OH",
"options": [
{ "id": "…", "label": "Ohio", "value": "OH" },
{ "id": "…", "label": "Texas", "value": "TX" }
],
"validation": null,
"errors": [],
"valid": true,
"messages": []
}
typeDrawformatSend as value
textText inputemail, url, zipcode, vin, drivers_license, or nullstring
numberNumber inputyear or nullnumber
maskedInput with a maskssn, fein, phone, currency, percentagestring or number
dateDate pickermonth_year or nullYYYY-MM-DD
selectDropdownan option's value
multiselectCheckboxes or a multi-selectarray of option values
radioRadio buttonsan option's value
searchSearch-as-you-type (the business type)business_classthe chosen choice's alias, see below
addressNothing: Bold Penguin builds it from the row's other fieldsdon't send it
checkboxA checkbox"1" when checked, null to clear it
labelText: show its label (and help) as a notenothing to send

Other types may appear as Bold Penguin adds them: skip a field whose type you don't recognise.

  • value is the saved answer, on every field that has been answered, whatever row it's in (side by side, an address, or an item such as a location). Fill the input with it, and send it back unchanged if the user doesn't change it. It's null when unanswered ([] for a multiselect with options).
  • label is the question. Show help under it and placeholder in the input.
  • options[].label is what to show; options[].value is what to send.
  • validation has the field's rules when it has any: min and max (for numbers the value, for text its length), min_date and max_date, pattern (a regular expression) and message (what to say when a rule fails). Check them as the user types.
  • readonly fields show a value the user can't change. An item's read-only fields are left out while the page has a warning about that item.
  • messages are messages to show with the question, above its input.

Search fields. For type: "search", look up choices as the user types:

{ "application_form_id": "<form id>", "answer_id": "<field id>", "filter": "carpentry", "per_page": 10 }

Each choice has a value (to show) and an alias (the code to send, e.g. "238350"). Once saved, the field's value is the chosen choice, so show it as selected when the page loads.

Messages​

As the application is answered, it can pick up messages for the user, such as "Owner/officer information is optional for quoting, but will be required to bind." when workers' compensation is chosen, or a note that the policy start date was moved. Each is returned where it belongs:

WhereShow it
application_form.messages with location: nullAs a banner above the pages. Showing only the latest one is fine.
application_form.messages with location: "quote_presentation"On your quotes page
A page's messagesAt the top of the page
A field's messagesWith the question, above its input
{
"id": "…",
"code": "wc_location_owner_requirement",
"level": "info",
"priority": false,
"html": "Owner/officer information is optional for quoting, but will be required to bind.",
"text": "Owner/officer information is optional for quoting, but will be required to bind.",
"created_at": "…",
"updated_at": "…"
}
  • Style each message by level: info, warning or error. Messages with priority: true come first in each list.
  • html can contain links and formatting. Sanitize it before inserting it as HTML, or show text, the same message without markup.
  • code names the message (null for some), if you want to use your own wording.
  • Messages come and go as answers change. Render them from every form response, and from form.updated on the live updates socket, which is sent when one is added, changed or removed. They never count as new questions in changes.

Validity and errors​

  • valid is true when the field's value passes. A required field needs a value; an optional field that's empty is valid.
  • errors lists what fails: required, pattern, min, max, minDate, maxDate, followed by any message Bold Penguin returned. Show the field's validation.message if it has one.
  • A required field that hasn't been answered reports required straight away. Show errors once the user has touched the field or pressed Next, not when the page first loads.
  • A page's valid is true when all its fields are. It uses the same rules as Bold Penguin's own forms.

Saving​

Save each answer as the user gives it: choices and dates on change, text and numbers when the user leaves the field (or after a short pause in typing). Send autocomplete: true and the revision, then re-render from the response. An answer can add questions to the page the user is on, and saving it straight away shows them while the user is still there; saving only when they press Next would show them only after they tried to leave. Send one save at a time: anything changed while a save is out goes in the next one, with the revision from the latest response. A save can carry several answers when they change together.

{
"application_form": {
"field_values": [
{ "code": "mqs_state", "value": "OH" },
{ "code": "mqs_loc_address_1", "value": "1 Main St", "location_id": "<location id>" }
],
"autocomplete": true
}
}

For each field send { "code": field.code, "value": value }. A field inside a section item also carries the item's id (for example location_id); send it back under the same key.

A 422 means Bold Penguin rejected an answer. error.details.errors lists what's wrong, when it's known; show it on the fields and let the user fix them. See Errors.

New questions after a save​

An answer can add questions: to the page the user is on (say they pick a coverage that needs more detail), or as whole new pages. To tell the user what appeared, send back the revision of the form you rendered from with the save:

{
"application_form": {
"field_values": [ { "code": "CoverageTypes", "value": ["CGL", "AUTOB"] } ],
"autocomplete": true
},
"revision": "<revision from the form you rendered>"
}

Every response that returns the form has a revision. Keep the one from the form on screen and send it with each save. The save's response then has changes, comparing the two:

{
"has_changes": true,
"pages_added": [ { "id": "…", "title": "Vehicles", "fields": 12 } ],
"fields_added": [
{
"page_id": "…",
"page_title": "Business details",
"id": "…",
"code": "mqs_number_of_employees",
"label": "How many employees?",
"required": true
}
],
"fields_removed": 0,
"message": "1 new question was added to Business details. 1 new page was added: Vehicles."
}
FieldMeaning
has_changesWhether anything appeared or went away
fields_addedNew questions on pages that were already there. id is the new field's id, so you can highlight it. A question inside an item also has the item's id (location_id, …).
pages_addedPages that weren't there before, with how many questions each has
fields_removedHow many questions went away, because an answer switched them off
messageA sentence to show the user, or null when nothing changed

When has_changes is true, show message (a banner or a toast works) and highlight the fields in fields_added. Because each answer is saved as it's given, new questions on the page the user is on appear while they're still there; the page isn't valid until they're answered. Without a revision, changes is null. POST /location, POST /driver and the rest take revision too.

Repeat rows​

A part of the form the user can add more of is a repeat row, with one entry per item added so far:

{
"layout": "repeat",
"section": "locations",
"item": "location",
"label": "Locations",
"add_label": "Add Another Location",
"entries": [
{ "location_id": "<id>", "label": "1 Main St, Columbus, OH 43215", "remove_label": null, "rows": [ … ] },
{ "location_id": "<id>", "label": "New Location", "remove_label": null, "rows": [ … ] }
]
}

Show the row's label as the section's heading. Draw each entry's rows as a card or a collapsible block, with the entry's label as its heading, and put an add_label button below them that adds an item (see Creating sections). Give each entry a remove control too (see Remove an item), labelled with the entry's remove_label when it has one ("Delete Additional Insured") and your own text ("Remove") when it's null. The remove button is never one of the entry's fields.

Sections inside an item work the same way. Each location's rows can hold its own buildings, gl_classifications or jobs row, and every building, classification or job class added to it is a separate entry with its own id (gl_classification_id, job_id) and remove_label. Remove one with its own section and id, e.g. DELETE /application_forms/{id}/gl_classifications/{gl_classification_id}. The form shows "Remove Classification" only once a location has more than one classification. Until then remove_label is null, and the classification can still be removed.

When add_label is null, don't show an add button: items of that section are added on another page. The Buildings page, for example, lists each location to hold its buildings, so there the locations row has no add button and each location's buildings row has "Add Another Building".

The add button is offered even where some carriers limit the number of items (for example 20 locations).

Draw rows in the order they come, the add button with its repeat row. That's where Bold Penguin's own form puts them: a location's job classes, then "Add Another Job Class", then the location's own payroll fields.

Use label rather than numbering entries ("Location 1", "Location 2"): numbers change as items are added, and on the Buildings or Classifications pages they don't say which location is meant. For a location, label is its full address once the address is answered, and "New Location" until then. It's updated in every response, so refresh it after each save. Entries can contain repeat rows of their own, such as the buildings inside each location.

Live updates​

Instead of polling, open a WebSocket to the application and render from what it sends:

const ws = new WebSocket(`wss://<api host>/v1/application_forms/${formId}/events?token=${token}`);
ws.onmessage = (e) => {
const { event, data } = JSON.parse(e.data);
// …
};

Use the same Bearer token as for the other calls (as ?token=, since browsers can't send headers on a WebSocket). Every message is { event, application_form_id, data, sent_at }:

eventdataWhat to do
ready{ application_form }Render the form. Sent once connected.
form.updated{ application_form, changes }Re-render: application_form is the whole application as it is now, including questions a save just added. changes.message says what's new, as after a save. Also sent when the application's owner changes, so not_owner stays current, and when its messages change.
form.state_changed{ state, previous_state }E.g. show that quoting is done at quoted
form.progress{ percent_completed }Update a progress bar
quote_request.updated{ quote_request }Update that quote
quote_request.status_changed{ quote_request_id, request_status, previous_request_status }E.g. show a new quote at completed
quote_request.proposal_available{ quote_request_id, proposal }Offer the proposal download
quote_request.removed{ quote_request_id }Remove that quote; an answer change withdrew it
bind.status_changed{ quote_request_id, status, previous_status, policy }Show the purchase's progress: binding, pending_payment, then bound (with policy) or binding_failed
item.added, item.removed{ section, <item>_id, <parent>_id? } (the parent's id for a nested item, such as a building's location_id)A location, building, … was added or removed
bind.started, bind.review_started, bind.review_cancelled, bind.submitted{ quote_request_id, … }A purchase step was taken, for example in another tab
form.taken_over{ new_application_form_id, taken_over_by }Someone took the application over: stop editing, and open the new one if it's yours
process.updated, notificationBackground checks and notices; optional
error{ code, message }The connection is closing; see below

Closing. 4401 means the token is missing or expired, 4403 the token's user can't follow this application, 4404 there's no such application. For 4502 or a dropped connection, reconnect after a few seconds: ready sends the current application again.


Creating sections​

Some parts of an application repeat: a business can have several locations, each location several buildings, a commercial auto policy several vehicles and drivers, and so on. Each is a section, and each location, building or driver the user adds is an item in it.

  • Every item has an id named after it: location_id, building_id, driver_id. The same key appears on the repeat row's entries, on every field inside the item, and in your requests.
  • Some items exist already. For sections such as locations, buildings, drivers and vehicles, Bold Penguin creates the first item for you, when the application starts or when its parent is added. Answer it through POST /application_forms/{id}, using the id from the repeat row's entries. Only add items for the second one onwards.
  • Nested items need their parent. A building belongs to a location and a violation to a driver: send the parent's id (location_id, driver_id) when you add one.

Add an item with its answers​

One call creates the item and saves its answers. Each section has its own endpoint, named after its item.

{
"application_form_id": "<form id>",
"field_values": [
{ "code": "mqs_loc_address_1", "value": "1 Main St" },
{ "code": "mqs_loc_city", "value": "Columbus" },
{ "code": "mqs_loc_state", "value": "OH" },
{ "code": "mqs_loc_zip_code", "value": "43215" }
]
}
{ "location_id": "<new location id>", "application_form": { … } }

A building for that location:

{
"application_form_id": "<form id>",
"location_id": "<location id>",
"field_values": [
{ "code": "mqs_bldg_desc", "value": "Warehouse" },
{ "code": "mqs_bldg_year_built", "value": 1998 }
]
}

Send only questions from that section: every answer in the call goes to the new item. If the answers can't be saved, the item is removed again, so a failed call never leaves an empty item behind.

Add an empty item​

To add an item and answer it later, for example when the user clicks the add button:

POST /application_forms/{id}/locations                                 -> { "section": "locations", "location_id": "…" }
POST /application_forms/{id}/buildings { "location_id": "<location id>" } -> { "section": "buildings", "building_id": "…", "location_id": "…" }

The path ends with the repeat row's section. Then load the form to show the new entry, and save its answers with POST /application_forms/{id}, sending the item's id on each answer.

Remove an item​

Any item in any section can be removed, for example when the user clicks a remove button on its entry:

DELETE /application_forms/{id}/locations/{location_id}
DELETE /application_forms/{id}/buildings/{building_id}

The path ends with the repeat row's section and the entry's id.

{ "section": "locations", "location_id": "…", "application_form": { … } }
  • Items inside it are removed too: removing a location removes its buildings, classifications and jobs, and removing a driver removes its violations. Ask the user to confirm first; a removed item can't be brought back.
  • Re-render from application_form: it's the application without the item. Send the form's revision (?revision=…, URL-encoded) to get changes as after a save.
  • The section must match: removing a location through drivers is a 400 and removes nothing, when the item has answers on the form (which is how its section is known). Always use the item's own section.
  • Keep one item where the application needs one. Bold Penguin creates the first location, building, driver and vehicle for you; removing the last one leaves the section empty until the user adds another.

All sections​

The section reference has each section's questions, allowed values and an example request.

SectionHeadingAdd with answersItem idInsideFirst item
locationsLocationsPOST /locationlocation_id—Created for you
buildingsBuildingsPOST /buildingbuilding_idlocations (send location_id)Created for you
gl_classificationsGL classificationsPOST /gl_classificationgl_classification_idlocations (send location_id)Created for you
jobsJob classesPOST /jobjob_idlocations (send location_id)Created for you
ownersOwners and officersPOST /ownerowner_id—Created for you
gl_claimsGeneral liability claimsPOST /gl_claimgl_claim_id—You add it
bop_claimsBusiness owner's policy claimsPOST /bop_claimbop_claim_id—You add it
wc_claimsWorkers' compensation claimsPOST /wc_claimwc_claim_id—You add it
pl_claimsProfessional liability claimsPOST /pl_claimpl_claim_id—You add it
cl_claimsCyber claimsPOST /cl_claimcl_claim_id—You add it
flood_claimsFlood claimsPOST /flood_claimflood_claim_id—You add it
prop_claimsProperty claimsPOST /prop_claimprop_claim_id—You add it
crime_claimsCrime claimsPOST /crime_claimcrime_claim_id—You add it
dol_claimsDirectors and officers claimsPOST /dol_claimdol_claim_id—You add it
epli_claimsEmployment practices liability claimsPOST /epli_claimepli_claim_id—You add it
additional_insuredsAdditional insuredsPOST /additional_insuredadditional_insured_id—You add it
other_named_insuredsOther named insuredsPOST /other_named_insuredother_named_insured_id—You add it
subrogation_waiversWaivers of subrogationPOST /subrogation_waiversubrogation_waiver_id—You add it
additional_interestsAdditional interestsPOST /additional_interestadditional_interest_id—You add it
driversDriversPOST /driverdriver_id—Created for you
violationsViolationsPOST /violationviolation_iddrivers (send driver_id)You add it
accidentsAccidentsPOST /accidentaccident_id—You add it
claimsClaimsPOST /claimclaim_id—You add it
vehiclesVehiclesPOST /vehiclevehicle_id—Created for you
salespersonSalespeoplePOST /salespersonsalesperson_id—You add it
misc_partyScheduled partiesPOST /misc_partymisc_party_id—You add it
schedule_of_hazardsSchedule of hazardsPOST /schedule_of_hazardschedule_of_hazard_idlocations (send location_id)Created for you
shareholdersShareholdersPOST /shareholdershareholder_id—You add it
fireplacesFireplacesPOST /fireplacefireplace_id—Created for you
class_codesClass codesPOST /class_codeclass_code_idlocations (send location_id)Created for you
im_claimsInland marine claimsPOST /im_claimim_claim_id—You add it
gl_additional_interestGeneral liability additional interestsPOST /gl_additional_interestgl_additional_interest_id—You add it
bop_additional_interestBusiness owner's policy additional interestsPOST /bop_additional_interestbop_additional_interest_id—You add it
underlying_policiesUnderlying policiesPOST /underlying_policyunderlying_policy_id—Created for you
photo_scheduled_equipmentPhoto scheduled equipmentPOST /photo_scheduled_equipmentphoto_scheduled_equipment_id—Created for you
scheduled_equipmentScheduled equipmentPOST /scheduled_equipmentscheduled_equipment_id—Created for you

Your applications​

To build a dashboard of your applications, list them with the token from POST /auth/token or an agent's token. You see the applications your account has access to, usually every one in your tenant. A guest token sees none.

GET /application_forms?state=quoted&sort=updated_at&per_page=20
Authorization: Bearer <token>

Each application comes back as a dashboard row: its summary first, then its bind status, owner and quotes.

{
"application_forms": [
{
"id": "cd5ccf16-…",
"reference_id": "BP-1A2B3C",
"summary": { "business_name": "Acme Bakery LLC", "business_type": "Bakery", "email": "sam@acmebakery.com", "city": "Columbus", "state": "OH", "…": "…" },
"state": "quoted",
"percent_completed": 100,
"created_at": "2026-09-01T14:03:11Z",
"updated_at": "2026-09-02T09:40:52Z",
"bind": { "status": "bound", "payment_pending": false, "binding": false, "has_bound_policy": true, "dispositions": [] },
"owner": { "id": "…", "first_name": "Ada", "last_name": "Lee", "user_type": "end_user" },
"not_owner": false,
"quote_requests": [
{ "id": "…", "request_status": "completed", "quote_number": "Q-100", "carrier": { … }, "annual_price": 1234.0, "rate_type": "ready_to_bind", "online_bind": false, … }
]
}
],
"page": 1,
"per_page": 20,
"total": 41
}
  • summary has the answers a dashboard shows: business name and type, coverage types and start date, contact name, email and phone, and address, and more (such as the legal entity, employees, revenue and payroll). Every key is there, null when unanswered. summary.state is the business's state; the application's own state is beside it.
  • bind is the application's bind status; see below.
  • owner is who the application is assigned to, or null when nobody is.
  • not_owner is true when the application isn't yours (someone else owns it, or nobody does). Show a warning on those rows: you can view the application, but you have to take it over before you can change it. It's false for your own applications, and null if we couldn't tell who you are.
  • quote_requests has the application's quotes, each the full quote request object, the same as everywhere else. Withdrawn quotes are left out. It's null if the quotes couldn't be loaded; open them with GET /application_forms/{id}/quote_requests.
  • Ask for the next page while page × per_page < total.
  • GET /application_forms/counts returns { "new": 4, "in_progress": 9 } for headline numbers.

bind.status is the bind badge to show, the first that applies:

statusMeaning
payment_pendingThe customer was sent a payment link
bindingIn checkout, not yet bound
boundA policy was bound through Bold Penguin
bound_elsewhereThe agent marked it done as bound elsewhere
not_boundThe agent marked it done as not bound
nullNone of these

The flags behind it are also there (payment_pending, binding, has_bound_policy), with the agent's dispositions. Filter with labels=has_bound_policy for bound applications.

Filters, all optional:

QueryValues
qText to search the answers for (business name, email, …)
stateA form state, or all; repeat for several. Without one, every form except incomplete ones.
ownercurrent_user, not_current_user or unassigned; owner_id for one owner
created_after, created_beforeISO 8601 date-time
dispositionnone, bound, not_bound or dispositioned
labelshas_bound_policy: only applications with a policy bound through Bold Penguin
sort, orderA field (created_at, updated_at, state, percent_completed) or question code; asc or desc
page, per_pagePage from 1; 1–100 per page, 20 by default

The list can lag slightly behind the latest save.

Take over an application​

To work on an application yourself (one a customer started, or another agent's), take it over. You can tell which ones need it: not_owner is true on them in your applications, when you open one with GET /application_forms/{id}, and in the form that live updates send.

POST /application_forms/{id}/take_over
Authorization: Bearer <token>

201 with { application_form_id, taken_over_application_form_id, application_form }. Bold Penguin copies the application into a new application with the same answers, owned by you, and application_form is that new one, in the usual shape (without not_owner: it's yours). Use the new application_form_id from then on. It continues where the original was: every page whose answers are all in is complete, including the carriers' pages. A page with a missing or invalid answer stays open; once the user saves it, the pages after it appear. The carriers are the ones available to you, which can differ from the original's.

  • The original: marked as taken over, it drops out of your list, where the new one takes its place. Anyone following its live updates gets form.taken_over.
  • Buying its quotes: quotes on the original aren't the user's, so they can't be bought there (see someone else's quotes). Bind them on the new application.
  • Errors: 404 when the application doesn't exist or your account can't take applications over (guests can't); 409 when it's busy (retry); 422 when the copy failed (error.message says why).

Technical reference​

MethodPathAuthPurpose
GET/healthnoneService health check: { "status": "ok", "environment": "…", "api_version": "1.0" }
GET/storefronts/{code}noneYour storefront's settings, for a landing page
GET/userBearerThe signed-in user (/userinfo returns the same)
GET/auth/authorizenoneSign an agent in with SSO: redirects their browser
POST/auth/tokennoneGet a Bearer token: your client id and secret, an agent's username and password or SSO code, or a refresh token
POST/guestnoneStart an application; returns { token_id, token }
GET/tokens/{token_id}BearerLoad the application by token id: { id, application_form }
GET/application_formsBearerList your applications, for a dashboard
GET/application_forms/countsBearerCounts of new and in-progress applications
POST/application_formsBearerStart an application as the signed-in agent; returns the new application
GET/application_forms/{id}BearerLoad the application by its id
POST/application_forms/{id}BearerSave answers; returns the updated application
POST/{item} (/location, /building, /driver, …)BearerAdd an item with its answers
POST/application_forms/{id}/{section}BearerAdd an empty item
DELETE/application_forms/{id}/{section}/{item_id}BearerRemove an item and the items inside it
POST/application_forms/{id}/take_overBearerTake an application over: a copy owned by you
POST/choicesBearerSearch the choices for a search field
GET/application_forms/{id}/quote_requestsBearerThe quote requests, with their proposals
GET/application_forms/{id}/quote_requests/{quote_request_id}BearerOne quote request
POST/application_forms/{id}/quote_requests/{quote_request_id}/bindBearerStart buying a quote; returns { quote_request_id, retrieve_url, quote_request }
POST/application_forms/{id}/quote_requests/{quote_request_id}/selectBearerMark the quote the user picked
GET/attachments/{id}BearerDownload a proposal or policy document
WebSocket/application_forms/{id}/events?token=…?token=Live updates for the application, its quotes and purchases
various/application_forms/{id}/quote_requests/{quote_request_id}/…BearerCheckout in your app, for agents

Versions​

Every path is under /v1, and every HTTP response says which release answered in its API-Version header. Within v1:

  • Only additions. New endpoints, events, optional request fields, response fields (after the existing ones in each object) and new values in lists such as labels, trigger_type or a field's type. Ignore anything you don't recognise.
  • Nothing removed or changed. Removing or renaming anything, changing a field's type or meaning, or making a request field required waits for a new major version, /v2, which will run next to /v1 for a deprecation period. During that period v1 responses have Deprecation and Sunset headers.

The paths without /v1 (from before versions) still work the same way, but are deprecated: their responses have a Deprecation header, a Link header with the /v1 path, and a Sunset header once there's a date they stop working. Add /v1 to your base URL to move; the WebSocket is /v1/application_forms/{id}/events. A quote's attachment url (/attachments/{id}) is a path under the base URL: add it to your base URL, /v1 included (${API}${url}). Don't resolve it as a URL against the base URL, which drops /v1.

GET /storefronts/{code}​

No token needed. Every field is always there, null when unset:

{
"id": "…", "code": "abc123", "tenant_id": "…", "tenant_subdomain": "acme", "active": true,
"agency_name": "Acme Insurance", "url": "https://acme.example",
"logo_url": "https://…", "logo_url_full": "https://…", "partner_logo_url": null,
"primary_hex_color": "#003366", "accent_hex_color": "#ff9900",
"phone": "6145550100", "email": "hi@acme.example",
"address": { "street_1": "1 Main St", "street_2": null, "city": "Columbus", "state": "OH", "zipcode": "43215" },
"hours_by_day": { "monday": { "open": "09:00", "close": "17:00" } }, "timezone": "EST",
"landing_page_headline": "…", "landing_page_subheadline": null,
"faq_html": null, "footer_disclaimer_html": "<p>…</p>",
"show_quotes": true, "enable_captcha": false
}

400 for a code with characters other than letters, digits, - and _; 404 when there's no active storefront with that code.

GET /user​

The signed-in user. GET /userinfo returns the same.

{
"id": "…", "first_name": "Ada", "last_name": "Lee", "name": "Ada Lee",
"email": "ada@example.com", "phone": null, "user_type": "end_user", "active": true,
"permitted_to_bind": true,
"tenant": { "id": "…", "name": "Acme", "subdomain": "acme", "type": "agency", "logo_url": null },
"created_at": "2026-01-01T00:00:00Z"
}

permitted_to_bind is whether the user can buy policies, including checkout in your app. 401 without a valid token.

POST /auth/token​

{ "client_id": "<your client id>", "client_secret": "<your client secret>" }

200 with { "access_token": "…", "token_type": "bearer", "expires_in": 43200, "refresh_token": "…", "tenant_id": "…" }. Send access_token as Authorization: Bearer <access_token>. It lasts expires_in seconds (12 hours); get a new one when it expires, or refresh it. tenant_id is your tenant, for POST /guest. 401 when the id and secret don't match. Keep the secret on your server: never put it in a browser or app.

As a user, with a username and password. To act as one of your agents (their dashboard, their applications, taking applications over), sign in with their Bold Penguin username and password instead:

{ "grant_type": "password", "username": "ada@example.com", "password": "…", "client_id": "<your client id>" }
  • username is the agent's Bold Penguin username; for most agents it's their email.
  • client_secret is optional here. Send it if the call comes from your server.
  • A form body works too. Never put the password in the URL.
  • The response is the same, with tenant_id the agent's tenant. The token acts as the agent, so your applications and not_owner are theirs.
  • 401 with error.code invalid_grant for a wrong username or password, or unauthorized_client for an unknown client id. 400 if a field is missing.
  • Send the password straight to this endpoint over HTTPS and don't keep it; keep the token.

Sign in with SSO​

If your agents sign in to Bold Penguin through their company's identity provider (SAML, Google or Microsoft), send them through SSO instead of asking for a password. There are two steps.

1. Send the agent's browser (a link or redirect, not a fetch or an iframe) to GET /auth/authorize:

GET /auth/authorize?client_id=<your client id>&redirect_uri=<your callback URL>&state=<random>&login_hint=<their email>

They sign in with their identity provider and come back to <your callback URL>?code=…&state=….

  • redirect_uri must be registered for your client with Bold Penguin.
  • state is a random value you keep, and check matches when they come back.
  • login_hint (their email, or @<your tenant's subdomain>) picks the identity provider.
  • response_type defaults to code; keep it.

2. Exchange the code on your server, straight away. Codes expire 5 seconds after they're issued, and work once:

{ "grant_type": "authorization_code", "code": "…", "client_id": "<your client id>", "client_secret": "<your client secret>" }

The response is the same as above, and the token acts as the agent, as with a password. 401 with error.code access_denied when the code is wrong, expired or already used; start again from step 1.

Before SSO works, Bold Penguin has to register your callback URL for your client and set up your tenant's identity provider. Ask your Bold Penguin contact.

Refreshing a token​

Every token comes with a refresh_token. Exchange it for a new token instead of signing the agent in again:

{ "grant_type": "refresh_token", "refresh_token": "…", "client_id": "<your client id>", "client_secret": "<your client secret>" }

You get a new access_token and a new refresh_token. Each refresh token works once, so keep the new one. 401 means it's no longer valid; sign in again. Keep refresh tokens on your server, like the secret.

POST /guest​

FieldTypeRequiredNotes
tenant_idUUIDyesYour tenant
source_metadata.storefront_codestringyesYour storefront
source_metadata.recaptcha_tokenstringif captcha is onFrom the browser
sourcestringyesWhere the application came from: storefront, unless Bold Penguin gives you another value. It's one of a fixed set. Leaving it out, or sending a value not in the set, fails the request.
application_form.field_valuesarraynoAnswers to start with, as in a save

201 with { token_id, token }. 403 when the captcha token is missing or invalid, or the tenant doesn't allow new applications. A missing or wrong storefront code, or a missing source, fails the request too; check them if starting fails.

POST /application_forms​

Starts an application owned by the token's user: an agent signed in with a username and password or SSO. The body is optional:

FieldTypeRequiredNotes
application_form.field_valuesarraynoAnswers to start with, as in a save
application_form.autocompletebooleanno, but send true with answersCompletes the pages those answers finish
application_form.sourcestringnoWhere the application came from. Leave it out unless Bold Penguin gives you a value: it's one of a fixed set, and anything else is rejected with a 422. It's required when you send source_metadata.

201 with the new application, in the same shape as GET /application_forms/{id}. 401 without a valid token; 403 when the user can't start applications (guests start theirs with POST /guest); 422 when an answer was rejected or the application couldn't be created.

GET /tokens/{token_id} and GET /application_forms/{id}​

Both return the application in the same shape: the token read wraps it as { id, application_form }. Each read replaces the caller's token id; the new one is in cable_token. The Bearer token stays the same.

POST /application_forms/{id}​

FieldTypeRequiredNotes
application_form.field_values[].codestringyesThe field's code
application_form.field_values[].valuestring, number or arrayThe new value; see the field types. answer, its older name, still works.
application_form.field_values[].<item>_idUUIDfor fields in an itemThe field's item id, e.g. location_id. One per answer.
application_form.autocompletebooleanno, but send trueCompletes every page that's ready. This is what adds the next pages and sends quote requests.
revisionstringnoNext to application_form: the revision of the form on screen. The response then has changes; see New questions after a save.

field_values works the same way in every request that sends answers: this save, POST /guest, POST /application_forms and POST /{item}. Older apps may send it as answer_values, which still works; don't send both.

200 with the updated application, plus changes (null without a revision). 422 when an answer was rejected; error.details.errors lists them.

POST /{item}​

FieldTypeRequiredNotes
application_form_idUUIDyes
field_valuesarrayat least oneAs in a save; leave out the item id
<parent>_idUUIDfor nested sectionslocation_id for buildings, classifications, jobs, hazards and class codes; driver_id for violations. Other sections reject it.
revisionstringnoThe revision of the form on screen; application_form.changes then lists what the item added

200 with { <item>_id, <parent>_id?, application_form }.

POST /application_forms/{id}/{section}​

Optional body: the parent's id, for nested sections. 200 with { section, <item>_id, <parent>_id? }.

DELETE /application_forms/{id}/{section}/{item_id}​

Optional: revision, in the query or a JSON body. 200 with { section, <item>_id, application_form }. application_form is null in the rare case it couldn't be read back; the item is removed either way, so load the application again. 400 when the item's answers show it belongs to another section, 404 when the application has no such item.

POST /choices​

FieldTypeRequiredNotes
application_form_idUUIDyes
answer_idUUIDyesThe field's id
filterstringnoThe search text
page, per_pagenumbernoPagination; totals are in the X-Total, X-Page and X-Per-Page headers

POST /application_forms/{id}/quote_requests/{quote_request_id}/bind​

Optional body: { "action_type": "send_to_underwriting" }. 200 with { quote_request_id, retrieve_url, quote_request }; retrieve_url is null when the purchase continues inside Bold Penguin. 403 when the quote isn't the user's (take the application over and bind on the copy; see someone else's quotes), 422 when the quote can't be bought, 503 when Bold Penguin couldn't confirm who the user is (try again).

POST /application_forms/{id}/quote_requests/{quote_request_id}/select​

No body. 200 with { quote_request_id, quote_request }: see select a quote. 403 when the quote isn't the user's, 404 when there's no such quote, 503 when Bold Penguin couldn't confirm who the user is.

Checkout in your app​

Not offered yet: every quote has online_bind: false for now, so bind every quote with POST …/bind. The steps below are for when it's turned on.

All paths are under /application_forms/{id}/quote_requests/{quote_request_id}. Only the quote's owner can check out: every step answers 403 on someone else's quote (see someone else's quotes) and 503 when Bold Penguin couldn't confirm who the user is.

  1. POST …/bind_review: start. 201 when ready; 400 when checks failed (error.details.errors) or are still running (error.details.processes: try again shortly). This limits the application to this carrier until checkout ends; DELETE …/bind_review cancels it.

  2. GET …/configurable_texts: the legal texts to show, [{ id, code, content, requires_attestation }].

  3. POST …/attest { "attestation_codes": { "<code>": true } }: the user's agreement to each text that requires_attestation.

  4. POST …/payment_plan { "plan_id": "…" }: a plan from the quote's plans. Skip it when there's an integrated_bind:select_payment_plans text.

  5. POST …/payment_strategy { "origin_url": "<your page URL>" }: how to take payment. strategy is carrier_stripe (tokenize the card with Stripe.js using stripe_api_key), authorize_net (Accept.js), iframe (the carrier's own payment form, from iframe_details), stripe_payment_link or skip. origin_url must be the full http(s) URL of the page showing the form; from a browser it's taken from the Origin (or Referer) header when left out.

    For Hiscox (variant hiscox or hiscox_paymentus): choose a plan in step 4 first, and show iframe_url in an <iframe>. Hiscox loads the form's stylesheet from your page's origin (/assets/styles/hiscox-iframe.css, or hiscox-iframe-<version>.css for hiscox_paymentus; ask Bold Penguin for the version). With hiscox_paymentus, the payment_token is the data["reference-number"] of the success message the form posts to your page. With hiscox, the frame goes to /assets/iframe-callbacks/hiscox.html on your origin when the user finishes: serve a page there that posts its query string to the parent. code=000 means paid, and uID is the payment_token.

  6. POST …/bind_and_pay { "automatic_payments": false, "payment_token": "…" } to buy, or POST …/payment_pending for stripe_payment_link (the customer is emailed a link).

  7. Load the quote request until request_status is bound (or binding_failed, or pending_payment until a link is paid). The policy is then in its policy and attachments.

GET /attachments/{id}​

Optional query: disposition=inline to show the file rather than download it. 200 with the file and its Content-Type; 404 when there's no such attachment.

Errors​

Every error has the same body:

{ "error": { "status": 422, "code": "unprocessable", "message": "The request was rejected.", "details": { "errors": { "mqs_state": ["is invalid"] } } } }
  • code is for your code to check; message is a sentence to show or log, and may be reworded. Treat a code you don't recognise by its status.
  • details has more where these docs mention it (a save's errors, checkout's errors and processes). Otherwise it's null or holds other information from Bold Penguin; don't rely on it beyond what these docs describe.
StatuscodeMeaning
400bad_requestThe request is malformed: a missing field, a bad id, or a parent id the section doesn't take. message says which.
401unauthorizedMissing or expired token. Signing in uses invalid_grant, unauthorized_client or access_denied.
403forbiddenNot allowed: someone else's quote, or see POST /guest
404not_foundThe endpoint, application, item or answer doesn't exist
409conflictThe application is busy. Retry.
422unprocessableBold Penguin rejected an answer or a purchase; details.errors says what, when known
502bad_gatewayBold Penguin couldn't be reached. Retry.
503unavailableBold Penguin couldn't confirm who the user is. Retry.
{ "error": { "status": 400, "code": "bad_request", "message": "buildings items belong to locations; send location_id", "details": null } }

Standard questions​

Every application starts with the Eligibility pages: the questions Bold Penguin needs to find carriers and send quote requests. They are the same for every storefront. Carrier pages and sections add more questions later; the form shows them as they apply.

For each page below:

  • Questions you answer are always on the page.
  • Questions that depend on other answers appear once an earlier answer calls for them, for example the FEIN once the user says the business has one.
  • Filled in by Bold Penguin are hidden. Don't send them; they're worked out from other answers.

The form is the source of truth for what's required on a given application: each field has required.

Eligibility: General information​

Questions you answer

CodeQuestionTypeRequiredValues
mqs_first_namePolicyholder's First NametextYes—
mqs_last_namePolicyholder's Last NametextYes—
mqs_phonePhone Numbermasked (phone)YesExactly 10 characters. Must match ^(?!.*(\d)\1{9})[2-9][0-9]{9}$.
mqs_emailPolicyholder's email addresstextYesMust match (?=^.{1,254}$)^[a-zA-Z0-9/_%+-](?:[a-zA-Z0-9./_%+-]*[a-zA-Z0-9/_%+-])?@[a-zA-Z0-9](?:[a-zA-Z0-9.-]*[a-zA-Z0-9])?\.[a-zA-Z0-9]{2,}$.
mqs_business_nameBusiness NametextYesAt most 80 characters.
mqs_street_1Street addresstextYes—
mqs_street_2Suite / Apt NumbertextNo—
mqs_cityCitytextYes—
mqs_stateStateselectYes51 choices; use the field's options. For example AK, AL or AR.
mqs_zipcodeZipcodetext (zipcode)YesExactly 5 characters. Must match ^\d{5}$.
mqs_addressWhat is your address?addressNo—

Filled in by Bold Penguin: mqs_reference_id, mqs_route, mqs_tenant_transfer_reason, mqs_focused, mqs_agent_focused, mqs_consumer_focused_alternate_carrier, mqs_selected, mqs_consumer_selected, mqs_source_type, mqs_customer_metadata, mqs_full_name, mqs_title, mqs_middle_name, mqs_suffix, mqs_date_of_birth, mqs_full_street, mqs_language_preference.

Eligibility: Business type​

Questions you answer

CodeQuestionTypeRequiredValues
NaicsWhat kind of work do you do?search (business_class)YesSearch with POST /choices, using the field's id as answer_id, and send the chosen choice's alias (e.g. 238350 for Finish carpentry).

Filled in by Bold Penguin: mqs_naics_code.

Eligibility: Coverage type​

Questions you answer

CodeQuestionTypeRequiredValues
CoverageTypesCoverage typemultiselectYes40 choices; use the field's options. For example CGL, BOP or WORK.

Filled in by Bold Penguin: mqs_state_naics_coverage_current, mqs_state_naics_coverage_changed.

Eligibility: Business details​

Questions you answer

CodeQuestionTypeRequiredValues
mqs_coverage_start_datePolicy start datedateYesBetween today and 120 days from now.
mqs_year_establishedFounding Year of Businessnumber (year)YesBetween 423 years ago and this year. Must match ^\d{4}$.
mqs_legal_entityLegal Entity TypeselectYesOne of LLC, Sole Proprietorship, C Corporation, S Corporation, Partnership, Association, Trust, Joint Venture, Municipality or Other.
mqs_full_time_employeesFull-time employeesnumberYesAt least 0.
mqs_part_time_employeesPart-time employeesnumberYesAt least 0.
mqs_currently_insuredDo you currently have insurance for this business?radioYesOne of Yes or No.
mqs_ins_history_3yr_catchallHas this business had a lapse of coverage, been declined, canceled, or non-renewed in the last 3 years?radioYesOne of Yes or No.

Questions that depend on other answers

CodeQuestionTypeRequiredShown whenValues
mqs_years_of_experienceYears of experience in this industrynumberYes, when shownIt already has an answer (one Bold Penguin filled in)0 to 999.
mqs_has_feinDoes your business have a Federal Employer Identification Number (FEIN)?radioYes, when shownCarrier biberk can quote WCOne of Yes or No.
mqs_feinFEIN (Federal Tax Identification Number)masked (fein)Yes, when shownmqs_has_fein is YesMust match ^\d{9}$.
mqs_ssnSSN (Social Security Number)masked (ssn)Yes, when shownmqs_has_fein is NoMust match ^\d{9}$.
mqs_estimated_annual_revenueEstimated total gross revenuemasked (currency)Yes, when shownCoverage includes general liability, BOP, workers' compensation, EO, commercial property or cyber1 to 999,999,999.
mqs_number_of_ownersHow many owners are there in your business?numberYes, when shownCarrier acuity can quote CGL, AUTOB or BOP; or carrier hartford can quote BOP or GL; or carrier hiscox can quote GL; or carrier nextcarrier can quote BOP or GL; or carrier progressivecarrier can quote GL or BOP; or carrier rtspecialty can quote BOP or CGLAt least 1.
mqs_estimated_annual_payroll_ownerBusiness owner(s) onlymasked (currency)Yes, when shownCoverage includes general liability, BOP, workers' compensation, EO or commercial property1 to 999,999,999.
mqs_estimated_annual_payrollFull-time, part-time, and temporary employeesmasked (currency)Yes, when shownCoverage includes general liability, BOP, workers' compensation or EO and mqs_full_time_employees is more than 0; or coverage includes general liability, BOP, workers' compensation or EO and mqs_part_time_employees is more than 00 to 999,999,999.
mqs_estimated_annual_payroll_subcontractorsSubcontractorsmasked (currency)Yes, when shownCoverage includes general liability, BOP, workers' compensation, EO or commercial property0 to 999,999,999.
mqs_snow_removal_percentageWhat percentage of your annual sales involves snowplowing and ice removal?masked (percentage)Yes, when shownNaics is 561730 or 25 more0 to 100.
mqs_roof_workDo you perform any roofing work?radioYes, when shownNaics is 238350, 238160, 238390, 237990, 237310, 238220, 238910 or 978 moreOne of Yes or No.
mqs_remediation_workDo you perform any fire, mold or water remediation?radioYes, when shownNaics is 238350, 238160, 238390, 237990, 237310, 238220, 238910, 424690 or 1086 moreOne of Yes or No.
mqs_additional_insured_requiredAre you required to name a third party as an additional insured or loss payee on your policy? (i.e. building owner, bank, landlord, etc.)radioYes, when shownCoverage includes general liability, BOP, EO, commercial property, SURE, CRIM, EPLI, PLMSC, D&O, AUTOB, cyber, UMBRC or 27 more and your tenant isn't geico; or coverage includes general liability, BOP, workers' compensation, EO, commercial property, SURE, CRIM, EPLI, PLMSC, D&O, cyber, UMBRC or 27 more and your tenant is geicoOne of Yes or No.
mqs_loss_claim_5yr_catchallHas this business had a loss or claim in the last 5 years?radioYes, when shownThe tenant has feature claims_and_losses_mqsOne of Yes or No.
mqs_ins_history_3y_non_paymentWas it for non-payment?radioYes, when shownmqs_ins_history_3yr_catchall is YesOne of Yes or No.
mqs_covg_ca_liability_limitSelect the Bodily Injury & Property Damage for this commercial auto policy.selectYes, when shownYour tenant isn't geico; and coverage includes AUTOBOne of $300,000 combined single limit, $500,000 combined single limit or $1,000,000 combined single limit.
mqs_ca_refuse_quote_consentDo you understand that carrier(s) reserve the right to refuse to give you an auto quote?checkboxYes, when shownCoverage includes AUTOB and mqs_state is PASend "1" to check it.

Filled in by Bold Penguin: mqs_actual_years_in_business, mqs_tax_id, mqs_revenue_percent_subcontracted, mqs_employees, mqs_estimated_annual_payroll_owner_employees, mqs_estimated_annual_payroll_employees_subcontractors, mqs_estimated_annual_payroll_all, mqs_estimated_annual_payroll_percentage_subcontractors, mqs_annual_policy, mqs_services_contract, mqs_work_above_three_stories, mqs_provides_towing_services, mqs_swimming_pool_repair, mqs_dance_floor, mqs_subcontract, mqs_excavation_four_feet, mqs_framing, mqs_tract_housing, mqs_baby_furniture, mqs_firearms_on_premise, mqs_cooking_or_food_prep, mqs_gasoline_sales, mqs_window_cleaning, mqs_open_past_midnight, mqs_open_24_hours, mqs_percentage_offsite_catering, mqs_alcohol_sales, mqs_grilling_or_frying, mqs_vehicles_a_year_sold, mqs_install_eifs, mqs_covg_ca_uninsured_motorist_limit, mqs_vehicles_provide_tours, mqs_seasonal_worker_transport, mqs_progressive_auto_policy, mqs_tcpa_consent, mqs_jornaya_lead_id.

Eligibility: Building information​

Questions you answer

CodeQuestionTypeRequiredValues
mqs_building_percent_sprinkleredWhat percentage of the building is sprinklered?masked (percentage)Yes—
mqs_building_roof_materialWhat is the roofing material of the building?selectYesOne of Asphalt Shingle, Built-Up (Tar & Gravel), Clay or Concrete Tile, Metal, Single-Ply Membrane, Slate or Wood Shake.
mqs_building_roof_geometryWhat is the roof geometry of the building?selectYesOne of Flat, Gable, Hip or Pitched.
mqs_building_limitProvide an estimated value for the building limitmasked (currency)Yes0 to 999,999,999.

Questions that depend on other answers

CodeQuestionTypeRequiredShown whenValues
mqs_building_relationshipWhat type of building do you operate your business out of?selectYes, when shownCoverage includes BOP or commercial propertyOne of A building I lease, My home, A building I own and run my business out of, A building I own/manage and lease to others, An apartment, A commercial condo I lease, A kiosk or A commercial condo I own.
mqs_building_has_sprinkler_systemDoes the building contain an automatic sprinkler system?radioYes, when shownCoverage includes commercial propertyOne of Yes or No.
mqs_building_storiesHow many floors are in this building?numberYes, when shownCoverage includes BOP or commercial property1 to 255.
mqs_area_occupiedApproximately how many square feet does your business occupy of this building (not total square feet of building)? Enter 0 if you do not occupy the building.numberYes, when shownCoverage includes BOP or commercial property0 to 999,999,999.
mqs_building_square_footageWhat is the approximate total square footage of the building (including space you do not occupy)?numberYes, when shownCoverage includes BOP or commercial property1 to 999,999,999.
mqs_building_year_builtWhat is the approximate year the building your company occupies finished construction?number (year)Yes, when shownCoverage includes BOP or commercial propertyBetween 423 years ago and next year. Must match ^\d{4}$.
mqs_building_constructionWhich of the following describes the construction of your building?selectYes, when shownCoverage includes BOP or commercial propertyOne of Frame, Joisted Masonry, Masonry Non Combustible, Non Combustible, Fire Resistive or Modified Fire Resistive.

Filled in by Bold Penguin: mqs_dba, mqs_building_ownership, mqs_building_age.

Example: answering Eligibility​

Every question that is always required, in one save. Questions that depend on these answers come back in the response; answer them in the next save.

{
"application_form": {
"field_values": [
{ "code": "mqs_first_name", "value": "Jane" },
{ "code": "mqs_last_name", "value": "Doe" },
{ "code": "mqs_phone", "value": "6145550100" },
{ "code": "mqs_email", "value": "owner@example.com" },
{ "code": "mqs_business_name", "value": "Acme Carpentry LLC" },
{ "code": "mqs_street_1", "value": "1 Main St" },
{ "code": "mqs_city", "value": "Columbus" },
{ "code": "mqs_state", "value": "OH" },
{ "code": "mqs_zipcode", "value": "43215" },
{ "code": "Naics", "value": "238350" },
{ "code": "CoverageTypes", "value": ["CGL"] },
{ "code": "mqs_coverage_start_date", "value": "2026-11-01" },
{ "code": "mqs_year_established", "value": 2018 },
{ "code": "mqs_legal_entity", "value": "LLC" },
{ "code": "mqs_full_time_employees", "value": 1 },
{ "code": "mqs_part_time_employees", "value": 1 },
{ "code": "mqs_currently_insured", "value": "Yes" },
{ "code": "mqs_ins_history_3yr_catchall", "value": "Yes" },
{ "code": "mqs_building_percent_sprinklered", "value": 100 },
{ "code": "mqs_building_roof_material", "value": "Asphalt Shingle" },
{ "code": "mqs_building_roof_geometry", "value": "Flat" },
{ "code": "mqs_building_limit", "value": 25000 }
],
"autocomplete": true
}
}

Coverage types​

CoverageTypes is a multi-select. Send the codes, e.g. ["CGL", "BOP"]. The coverages chosen decide which pages and sections follow.

SendCoverage
CGLLiability
BOPBOP
WORKWorkers Compensation
EOProfessional Liability (E&O)
PROPCommercial Property
SUREBond
CRIMCrime
EPLIEmployment Practices Liability
PLMSCMiscellaneous Professional Liability
DODirectors & Officers Liability
AUTOBCommercial Auto
CYBERCyber Liability
UMBRCUmbrella
FLOODCommercial Flood
MMALMedical Malpractice
INMRCInland Marine
OTHEROther/Not Sure
CAVNAviation
BANDMBoiler & Machinery
BLDRBuilder's Risk
DICDifference in Conditions
GARAGGarage & Dealers
LLLiquor Liability
OCPOwners & Contractors Protective Liability
POLLPollution Liability
SESpecial Events
BOATWatercraft
ALAuto Liability
APDAutomobile Physical Damage
CALCondo Association Liability
EBEquipment Breakdown
ELEnvironmental Liability
EQEarthquake Insurance
FLFiduciary Liability
HNOAHired and Non-Owned Auto Liability
MTCMotor Truck Cargo
PRODLIABProduct Liability
PWRPower Outage Coverage
VPVacant Property
XLExcess Liability

After Eligibility​

Once the Eligibility pages are complete, Bold Penguin adds pages for what the business needs. Which ones depends on the coverages chosen and your storefront's setup:

PageAdded whenSections on it
Additional detailsThe coverage includes BOP, general liability, workers' compensation or commercial property (and, for claims history, other lines)locations, buildings, gl_classifications, jobs, owners, the claims sections
VehiclesThe coverage includes commercial auto (AUTOB)vehicles
DriversThe coverage includes commercial auto, after Vehiclesdrivers, violations
Carrier pagesOne per carrier that can quote, once the pages above are completeThe carrier's own questions and sections

Completing a carrier's page sends that carrier a quote request (see How to get a quote).