DOCS

Complete a questionnaire

Complete a questionnaire

Answer and finalize compliance questionnaires through the Zonos API — with AI prefill, a guided wizard, or a single one-shot submission.

GraphQL

Once you know which programs apply (see Check compliance requirements), Clarify gives you three ways to answer them:

  • AI-assisted (recommended) - infer and prefill answers, then attest the ones the importer agrees with.
  • Guided wizard - start a session and submit answers question by question, with branching handled server-side.
  • One-shot - submit a complete set of answers in a single call.

All of these write a response: an immutable record whose effect-keyed answers materialize onto the item for customs filing.

Scope required

Reading sessions and responses requires the COMPLIANCE_READ scope; submitting answers requires COMPLIANCE_WRITE. See OAuth.

Answer values are JSON-encoded

Every answer value is a JSON-encoded string. A boolean is "true", a select or text value is a quoted string such as "\"cosmetic\"", a multi-select is "[\"a\",\"b\"]", and a number is "42". Encode accordingly when you submit, and decode when you read.

complianceQuestionnaireInfer resolves the applicable programs for your items, opens a session, and pre-answers questions from inferred facts. Each inferred answer has provenance: INFERRED and a confidence score. Lines are not auto-finalized — fully-answered lines wait for you to attest them.

Pass intendedUse to backstop inference; storefront flows should send CONSUMER.

1mutation InferQuestionnaire($input: ComplianceQuestionnaireInferInput!) {
2 complianceQuestionnaireInfer(input: $input) {
3 session {
4 id
5 status
6 lines {
7 id
8 programId
9 status
10 currentQuestion {
11 code
12 prompt
13 answerType
14 }
15 visibleQuestions {
16 question {
17 code
18 prompt
19 answerType
20 required
21 }
22 answer {
23 value
24 provenance
25 confidence
26 }
27 }
28 }
29 }
30 lines {
31 lineId
32 programId
33 targetId
34 state
35 inferredCount
36 remainingRequiredCount
37 }
38 alreadySatisfied {
39 programId
40 targetId
41 }
42 }
43}

Each line's state tells you what to do next:

  • READY_TO_ATTEST - every required, visible question is answered. Attest it.
  • PARTIAL - some answers were inferred but required questions remain. Resume the wizard at currentQuestion.
  • NOT_INFERRED - nothing matched; answer it as a plain questionnaire.

Programs already satisfied by a current response are returned in alreadySatisfied and skipped (no line is created for them).

Review and correct inferred answers 

After inference, the session holds pre-filled answers — not a finalized response. The end user (for example, a FedEx shipper) reviews them, and for each answer does one of two things:

  • Agrees - leaves the answer untouched. It stays INFERRED.
  • Disagrees - submits a corrected value with questionnaireAnswersSubmit to the same session line. The correction supersedes the inferred answer, which becomes MANUAL.

They only touch the answers they want to change — there's no separate "manual mode," and no need to re-enter the answers they accepted.

Accepted inferences are kept, not discarded

Disagreeing with one inference overrides only that answer. When the line is finalized, the response captures the full answer set as a blend: the accepted inferred answers (recorded as INFERRED) plus the corrected ones (recorded as MANUAL). Every required answer is on the response, each stamped with its provenance — a built-in record of what the AI supplied versus what a person changed.

Suppose a session came back with two inferred answers — intended_use (high confidence) and contains_color_additives (low confidence). The shipper accepts intended_use but corrects contains_color_additives, so they submit only that one answer:

1mutation CorrectAnswer($input: QuestionnaireAnswersSubmitInput!) {
2 questionnaireAnswersSubmit(input: $input) {
3 lines {
4 id
5 status
6 responseId
7 answers {
8 questionCode
9 value
10 provenance
11 confidence
12 }
13 }
14 }
15}

The accepted intended_use answer stays INFERRED; the corrected contains_color_additives is now MANUAL. Both land on the finalized response. If the shipper had agreed with everything, they would skip the corrections and use Attest inferred answers (below) instead. The full submission mechanics are covered under Submit answers.

Option 2: Start a session manually 

If you would rather drive answering yourself, start a session directly with the programs you discovered. questionnaireSessionStart fans out one line per (item, program) and returns the first question for each line.

1mutation StartSession($input: QuestionnaireSessionStartInput!) {
2 questionnaireSessionStart(input: $input) {
3 id
4 status
5 lines {
6 id
7 programId
8 targetType
9 targetId
10 status
11 currentQuestion {
12 code
13 prompt
14 answerType
15 required
16 }
17 }
18 }
19}

Submit answers 

Whether the session came from inference or questionnaireSessionStart, submit answers to a line with questionnaireAnswersSubmit. You can send one answer or several at once. The server evaluates display conditions, advances currentQuestion to the next required, visible, unanswered question, and — when none remain — completes the line and finalizes its response (responseId is populated). The session completes once every line is COMPLETED.

Editing an inferred answer simply supersedes it with a MANUAL one.

1mutation SubmitAnswers($input: QuestionnaireAnswersSubmitInput!) {
2 questionnaireAnswersSubmit(input: $input) {
3 id
4 status
5 lines {
6 id
7 status
8 responseId
9 currentQuestion {
10 code
11 prompt
12 }
13 answers {
14 questionCode
15 value
16 provenance
17 confidence
18 }
19 }
20 }
21}

Attest inferred answers 

When a line came back READY_TO_ATTEST from inference, you don't need to resubmit the answers — accept them in one call with questionnaireSessionAttest. It finalizes every eligible line (or only the lineIds you pass) and rolls the response provenance up to attested.

1mutation AttestSession($input: QuestionnaireSessionAttestInput!) {
2 questionnaireSessionAttest(input: $input) {
3 id
4 status
5 lines {
6 id
7 status
8 responseId
9 }
10 }
11}

To abandon an in-progress session instead, call questionnaireSessionAbandon(id: ID!).

Option 3: Submit a complete response in one shot 

If you already have every answer — for example, a durable answer set for a CATALOG_ITEM — skip the session entirely and submit directly with questionnaireResponseSubmit. It validates the answers against the program's ACTIVE questionnaire (no answer for a hidden question; every required visible question answered) and persists a COMPLETE response, superseding any prior complete response for the same (program, target, route).

1mutation SubmitResponse($input: QuestionnaireResponseSubmitInput!) {
2 questionnaireResponseSubmit(input: $input) {
3 id
4 status
5 targetType
6 targetId
7 completedAt
8 answeredBy
9 answers {
10 questionCode
11 value
12 provenance
13 }
14 }
15}

Read sessions, responses, and readiness 

Fetch an in-progress session with questionnaireSession(id), a finalized record with questionnaireResponse(id), and an all-or-nothing rollup for a whole container with containerComplianceReadiness.

1query ContainerReadiness($containerType: ContainerType!, $containerId: ID!) {
2 containerComplianceReadiness(
3 containerType: $containerType
4 containerId: $containerId
5 ) {
6 ready
7 totalLineCount
8 outstandingLineCount
9 }
10}

ready is true only when the container has at least one line and every line has a COMPLETE response — a convenient gate before you file the entry.

To read a finalized response and its answers:

1query QuestionnaireResponse($id: ID!) {
2 questionnaireResponse(id: $id) {
3 id
4 status
5 targetType
6 targetId
7 shipToCountry
8 completedAt
9 answeredBy
10 answers {
11 questionCode
12 value
13 provenance
14 confidence
15 answeredAt
16 expiresAt
17 }
18 }
19}

Next steps 

Book a demo

Was this page helpful?