Skip to main content
A company files one annual report per fiscal year with the state it was formed in. The Partner API covers that whole cycle: two reads that tell you what to collect and what it costs, a submission that records the report and fixes its price, a second call that confirms it, and two reads that follow it to completion. Submission is two calls, not one. The first puts the report on file and fixes the state fee on it. The second confirms the report and hands it to doola to file. doola charges nothing through this API: the fee is what doola settles with you outside it, and the split exists so you can collect from your own customer on your own timing.

The flow

Two reads stand outside this sequence. The compliance calendar tells you when a report is due, and the annual-report reads below tell you where any report stands at any point.

Requirements: what to collect

This endpoint is read-only. It describes what to collect; it neither starts a filing nor stores anything.
An annual report asks for two different kinds of information: questions the state puts to the company, and company details doola already holds. This endpoint returns both as one ordered list, so you render the list you are given rather than deriving it from the state yourself. Most states ask no questions at all. For those companies the response carries only the company details, which is the common case rather than an edge one.
The full schema is in the Compliance section of the API reference. A Wyoming LLC, abbreviated:

Field groups

Every entry carries a group, and the two groups behave differently. Read the group rather than guessing from the fieldId. Questions are listed first, then the company details. required tells you whether the filing is rejected without the field. A COMPANY_DETAIL with required: false is data doola already holds, so you do not have to ask the customer for it. A COMPANY_DETAIL with required: true is data we do not hold and cannot file without; today that is the phone number and physical address of a Delaware C-corp.

Rendering a field

Treat the list as data. New type values and new fields are added over time, so a client that switches on a closed set of ids will break; one that renders whatever it is handed will not.
This endpoint tells you what to collect; sending it back is the submission, which accepts exactly the fields published here, keyed by fieldId.
Do not derive the requirements from the compliance calendar, or the other way round. The two answer different questions from different sources: whether doola files an annual report for the state, and whether an obligation is currently tracked for the company. For a small number of states they do not line up, so a tracked due date is not a promise that this endpoint returns a field list, and a 422 here is not a promise that the calendar reports the state as unsupported.

State fees: what it costs

This endpoint is read-only. It reports a price; it neither starts a filing, stores anything, nor charges the customer.
Every state doola holds annual-report data for answers with a number. There is nothing to send and nothing to collect first: one call, one amount.
No body, no query parameters. The fee does not vary by year today.
Amounts are cents so there is no decimal to round. priceInCents is never null; a state that charges nothing reports 0. Two flags decide how to read that amount, and they answer different questions:
  • isFilingRequired — is there an annual report to file for this company at all?
  • isMinimum — is this the final charge, or the state’s floor?
Read both before acting on priceInCents. A zero on its own is ambiguous, and so is a number. The amount here is a quote, read off what doola holds about the company. The amount charged is fixed when the report is submitted, from the figures the submission carries, and comes back on it as priceInCents. Where doola already holds every figure the formula needs, the two are the same number.

A floor is not a quote

Some reports cannot be priced exactly before the figures exist — Wyoming’s, from its asset worksheet, and a Delaware C-corp’s when doola holds no authorized share count. Rather than refusing to answer, the endpoint reports the state’s floor and sets isMinimum: true:
Wyoming’s floor is the state’s own minimum plus its fixed processing fee, so it is a real charge and not a placeholder, and the final amount is never below it. What settles that amount is the submission: the report is priced from the figures it carries, the Wyoming asset worksheet or a Delaware C-corp’s authorizedShares in companyDetails, and the result comes back on it as priceInCents. A formula is not the same as a floor. Delaware computes a C-corp’s fee from its authorized share count: when doola holds that count the amount is exact and isMinimum is false. When it does not, the formula falls to its lowest bracket and isMinimum is true — the amount is real, but the company’s own fee is at least that and often more. Send the count as authorizedShares in companyDetails when you submit, and the report is priced from it rather than from the bracket. Computed and provisional are different things; only isMinimum separates them.
A state joining this list is not a breaking change — isMinimum simply comes back true for more companies. Branch on the flag, never on the state code.

Which answers move the fee

affectsFee on a requirements field marks the fields the state computes its charge from. It is false on every field for almost every company, because most states price the report from data doola already holds — the answers are for the filing, not for the price. Where it is true, the state’s formula reads that field: the Wyoming asset figures and the authorized share count on a Delaware CCorp. The price fixed when the report is submitted follows the values you send for them, and comes back as priceInCents. affectsFee and isMinimum answer different questions. affectsFee says the state’s formula reads that field. isMinimum says the quoted amount is still only the state’s floor, which happens when doola does not hold the figure the formula needs — always for Wyoming, and for a Delaware CCorp with no share count on file. A Delaware CCorp whose shares doola already holds is quoted exactly, so isMinimum is false while affectsFee stays true on the share count. One with no count on file is quoted the floor, and priced exactly once you send the count with the report. Use affectsFee to tell the customer which figures affect what they will pay. Do not use it to decide whether to collect a field — that is required.

When no report is filed at all

Whether a report is filed depends on the state and the entity type together. Alabama and Ohio require none from either, while in New Mexico and South Carolina only the LLC is exempt — a CCorp in either files and pays a fixed fee. Both reads publish the same isFilingRequired for the same company, from the same source, so they never disagree. The requirements answer 200 with an empty fields list:
The state fees answer 200 with priceInCents: 0 and isFilingRequired: false. Some states file an annual report and charge nothing for it; others file no annual report at all. Both report 0, and only isFilingRequired tells them apart.
Do not branch on priceInCents === 0. Branch on isFilingRequired. Treating a no-report company as free-but-filable would have you collect answers and attempt a filing that cannot be made.
On the requirements, fields: [] means two different things, and isFilingRequired is what separates them. With isFilingRequired: true it would mean the state asks nothing beyond the company details — but the company details are always appended, so that case never actually produces an empty list. With isFilingRequired: false the list is empty because no filing exists.
isFilingRequired: false is a normal answer, not an error — on the two reads. The submission is the one call where it is an error, 422 E_STATE_NOT_SUPPORTED: there is nothing to submit.

Submitting the report

This call records the report and fixes its state fee. Confirming it is a separate step, on your own timing, and charges the priceInCents returned here.
Read the requirements first. This call takes the answers to the questions it published, plus any corrections to the company details doola holds, and puts the report on file.
Every key in companyDetails means “use this instead of what doola has”. Leaving a key out means “keep what doola has” — you cannot clear a value. A corrected address replaces the one on file whole, so a partial address is rejected rather than merged.
This is the same report shape the reads publish, with isReplaced added, so one generated type covers the submission, the payment confirmation and both reads.

What is accepted

Only what the requirements published for this company. The check runs against the same list GET /requirements returns, so the two cannot disagree.
  • An answer to a field the company was not offered is refused with 400, naming the field. It is never dropped silently.
  • A field behind a gate is only demanded, and only accepted, while the gate is open. Wyoming’s Q16 (“do you hold assets?”) gates its asset figures: answer no and none of them are asked for — send one anyway and it is refused.
  • Every required field that applies has to be present. For a Delaware C-corp that is phoneNumber and physicalAddress, the two details doola does not hold.
  • Values are typed by the field’s type: NUMBER and AMOUNT are non-negative decimals with at most two places, SELECT and RADIO come from the published options, DATE is yyyy-MM-dd.
  • authorizedShares and issuedShares are counts, so they have to be whole numbers. perValuedShares is money per share and keeps up to eight decimal places, enough for a $0.0001 par value; a ninth is refused rather than rounded away.

Submitting twice

One report per company per fiscal year. A second submission for the same year replaces the first while nothing has been paid for it and no payment is in progress — the answers are replaced whole, the report keeps its annualReportId, its price is fixed again from the new figures, and the response is 200 rather than 201, with isReplaced: true. That is how you correct a mistake before paying, at no cost, and how a Wyoming filing converges on its exact amount from the floor. Once a payment for the report is in progress, or it has been paid for, filed, or is being worked on, it is not overwritten: a second submission answers 409 E_ANNUAL_REPORT_ALREADY_EXISTS. There is no deadline on the unpaid state. A report sits at FILLED indefinitely, and resubmitting is what frees the year’s slot.

Confirming the report

This call hands the report to doola to file. Nothing is charged: doola settles the state fee with you outside this API, so confirming is you committing to the filing. Call it when your own customer has paid you — before or after is your decision.
No body. It answers with the same report shape the submission returns. The report moves straight to IN_PROGRESS, at which point doola opens the filing task and sends company_annual_report_submitted. From then on the report can no longer be replaced. A priceInCents of 0 does not skip this call. The confirmation is still yours to make, and the filing starts either way. Calling confirm twice is safe. Once the report has moved past FILLED the second call is a no-op that answers 200 with the report unchanged.

Reading reports back

These two endpoints are read-only. They show what the submission put on file and where doola has taken it since.
A company has at most one annual report per fiscal year. The list tells you which reports doola holds and where each stands; the single read adds the answers on record for one of them. Both address a report by the annualReportId the submission returned.

Listing a company’s reports

Newest fiscal year first. Every key is always present; a value doola does not hold is null, never dropped.
An empty list means doola holds nothing to show for the company, not that the company does not exist. A company that does not exist, or is not yours, is a 404.

Reading one report

The same fields as one entry of the list, plus answers: what was answered for this report. Most states ask no questions, so an empty list is the common case.

Status

One vocabulary across every annual-report response, so a report reads back under the status it was returned with. There is no rejected status. A report the customer filed themselves, directly with the state, is reported as FILING_COMPLETED: it is filed, and hiding it would let you think the year is unfiled and file it a second time.
Every report you submitted is listed, under one of the statuses above. The single exception is a report doola’s own app opened that nobody has acted on yet: it is absent from the list and not readable by id either, so the two reads never disagree about what exists. Read an unfamiliar status as informational rather than switching on the list exhaustively.
submittedAt is not reliably populated. It is stamped on only some of the routes a report can take through doola’s pipeline, so a report can be filed and still carry no date. Read status to know where a report stands. Treat a missing submittedAt as no information, never as evidence that a report was not submitted.
There is no channel yet for supplying what AWAITING_INFORMATION is waiting on. When one lands it will be a required action, which is how doola already asks a partner for something during formation, but no annual-report action code exists today, so do not poll for one.

Webhooks

You do not have to poll a report to completion. Two webhooks mark the ends of the flow: company_annual_report_submitted once you confirms and doola starts the filing, and company_annual_report_completed once the state has the report, which is terminal. document_annualreport_uploaded fires separately when the filed report is stored and downloadable — independently of the two above, so do not assume an order.

Errors

404 E_NOT_FOUND is deliberately undiscriminating: a report that does not exist, one belonging to another company, and one in a state you are not shown are all the same answer. Telling them apart would confirm that an id you may not read is nonetheless real.
422 should not occur for a company created through this API. Every US state and DC is covered for both LLC and CCorp, so it is declared for completeness — a state added before its pricing lands, an entity type outside this API’s reach, or a pricing row carrying no amount. The last is a fault on doola’s side, and only the state fees can hit it: that read refuses rather than quoting 0, which would be indistinguishable from a state that files at no charge, while the requirements never read an amount. A company that simply files no annual report is not a 422 on either read: they answer 200 with isFilingRequired: false. On the submission it is, because there is nothing to submit.
There is no 400 on the state fees: the request carries nothing to validate. Ownership is enforced the same way across the Partner API: a company that exists but isn’t yours returns 404, never 403, so existence cannot be probed across partners. See Errors.