> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sumvin.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Set up an Errand

> Set up an Errand — created via an IPA that returns 202 Accepted with a preflight workflow — and observe its lifecycle from qualifying through to an approved state.

<Snippet file="product-term-disambiguation.mdx" />

At the end of this page, you will have set up an **Errand**. On the wire you create an `IPA` (the durable intent record) that returns `202 Accepted` with a `preflight_workflow_id`, poll it through the lifecycle, and approve it — landing the `IPA` record in the `approved` state, ready for the agent to begin executing the Errand.

The create call:

* [`POST /v0/user/ipa`](/api-reference/ipa/create-ipa) — Create an IPA.

An Errand is the durable contract between a user and an agent (an Envoy). It captures the intent — what the user wants, what the agent is allowed to do, for how long, and under what constraints — and follows the **Intent → Authorization → Vigilance** arc. On the wire, an Errand is created and tracked as an `IPA` record. Once approved, that record drives the full Errand workflow.

<Note>
  This quickstart uses the current `IPA` model — `preflight_workflow_id`, `originating_chat_id`, `originating_agent_task_id`, and the `originating_agent_task` / `executing_agent_task` expansions. Treat this quickstart as the source of truth for `IPA` request and response shapes.
</Note>

## Prerequisites

* A Sumvin-verified user with a deployed Safe. If you don't have one, run the [stand up an account](/guides/quickstarts/stand-up-account) quickstart first.
* A <Tooltip headline="Stamped Mandate" tip="A Stamped Mandate (PINT) — an EIP-712 signed message authorising specific scopes and actions." cta="Glossary →" href="/glossary">Stamped Mandate</Tooltip> that grants the scopes this Errand needs. See [Stamped Mandates](/identity/pint) for the mint flow and [scopes](/identity/scopes) for the full catalog. The create call does not name a mandate — the Errand's authorisation is settled from the scopes on the caller's own Stamped Mandate, and what may be spent is settled at the draw.
* A Platform API JWT. See [authentication](/authentication).

<Steps>
  <Step title="Mint a Stamped Mandate for the Errand">
    Errands need scopes like `sr:us:pint:perpetual:search` plus any downstream action scopes (for example `sr:us:pint:spend:execute` if the Errand will authorise purchases).

    The Stamped Mandate mint flow is covered in full at [Stamped Mandates](/identity/pint). You never pass its URI to `POST /v0/user/ipa` — the create call carries no mandate reference.
  </Step>

  <Step title="Create the IPA">
    Post the user's intent, autonomy preferences, and constraints. Include the `Idempotency-Key` header so retries do not create duplicate records.

    <CodeGroup>
      ```bash curl theme={null}
      curl -X POST https://api.sumvin.com/v0/user/ipa \
        -H "x-juno-jwt: <your-jwt-token>" \
        -H "x-juno-orgid: <your-org-id>" \
        -H "Content-Type: application/json" \
        -H "Idempotency-Key: ipa-req-2026-04-16-0001" \
        -d '{
          "raw_intent": "Find me a pair of Nike Air Max 90 in size 10, under $150",
          "intent_type": "product",
          "autonomy_level": "approve_before_purchase",
          "constraints": { "max_price": 150.00, "currency": "USD" },
          "originating_chat_id": "chat_xyz789"
        }'
      ```

      ```typescript TypeScript theme={null}
      const res = await fetch("https://api.sumvin.com/v0/user/ipa", {
        method: "POST",
        headers: {
          "x-juno-jwt": process.env.SUMVIN_JWT!,
          "x-juno-orgid": process.env.SUMVIN_ORG_ID!,
          "Content-Type": "application/json",
          "Idempotency-Key": "ipa-req-2026-04-16-0001",
        },
        body: JSON.stringify({
          raw_intent: "Find me a pair of Nike Air Max 90 in size 10, under $150",
          intent_type: "product",
          autonomy_level: "approve_before_purchase",
          constraints: { max_price: 150.0, currency: "USD" },
          originating_chat_id: "chat_xyz789",
        }),
      });
      const { intent } = await res.json();
      ```

      ```python Python theme={null}
      import httpx
      import os

      res = httpx.post(
          "https://api.sumvin.com/v0/user/ipa",
          headers={
              "x-juno-jwt": os.environ["SUMVIN_JWT"],
              "x-juno-orgid": os.environ["SUMVIN_ORG_ID"],
              "Idempotency-Key": "ipa-req-2026-04-16-0001",
          },
          json={
              "raw_intent": "Find me a pair of Nike Air Max 90 in size 10, under $150",
              "intent_type": "product",
              "autonomy_level": "approve_before_purchase",
              "constraints": {"max_price": 150.00, "currency": "USD"},
              "originating_chat_id": "chat_xyz789",
          },
      )
      intent = res.json()["intent"]
      ```

      ```tsx React theme={null}
      import { useCreateIPA } from '@sumvin/api-hooks';

      function CreatePerpetualSearchButton() {
        const { mutate, isPending } = useCreateIPA();

        const handleCreate = () => {
          mutate({
            raw_intent: "Find me a pair of Nike Air Max 90 in size 10, under $150",
            intent_type: "product",
            autonomy_level: "approve_before_purchase",
            constraints: { max_price: 150.0, currency: "USD" },
          });
        };

        return <button onClick={handleCreate} disabled={isPending}>Create IPA</button>;
      }
      ```
    </CodeGroup>

    <Tip>
      Using TanStack Query? `useCreateIPA` invalidates the IPA list and agent-task queries on success. To forward an `Idempotency-Key` header, see the [mutation patterns](/guides/tanstack-query-patterns#mutation-patterns) page for the recommended wrapper shape.
    </Tip>
  </Step>

  <Step title="Inspect the 202 Accepted response">
    **Response:** `202 Accepted`

    ```json theme={null}
    {
      "intent": {
        "id": "ipa_1a2b3c4d",
        "status": "qualifying",
        "raw_intent": "Find me a pair of Nike Air Max 90 in size 10, under $150",
        "intent_type": "product",
        "autonomy_level": "approve_before_purchase",
        "constraints": { "max_price": 150.00, "currency": "USD" },
        "preflight_workflow_id": "wf_preflight_9a7d2f",
        "originating_chat_id": "chat_xyz789",
        "originating_agent_task_id": null,
        "created_at": 1740000000000
      },
      "_links": {
        "self": { "href": "/v0/user/ipa/ipa_1a2b3c4d" },
        "events": { "href": "/v0/user/ipa/ipa_1a2b3c4d/events" },
        "answers": { "href": "/v0/user/ipa/ipa_1a2b3c4d/answers", "method": "PUT" },
        "decision": { "href": "/v0/user/ipa/ipa_1a2b3c4d/decision", "method": "PUT" },
        "cancel": { "href": "/v0/user/ipa/ipa_1a2b3c4d", "method": "DELETE" }
      }
    }
    ```

    The `preflight_workflow_id` points to the workflow run that parses and qualifies the intent. The `IPA` status starts as `qualifying` while preflight runs.
  </Step>

  <Step title="Poll the IPA status">
    Fetch the IPA until it transitions out of `qualifying`. Use the `originating_agent_task` expansion to see the preflight agent task in the same response.

    ```bash theme={null}
    curl "https://api.sumvin.com/v0/user/ipa/ipa_1a2b3c4d?expand=originating_agent_task" \
      -H "x-juno-jwt: <your-jwt-token>" \
      -H "x-juno-orgid: <your-org-id>"
    ```

    **Response:** `200 OK`

    ```json theme={null}
    {
      "intent": {
        "id": "ipa_1a2b3c4d",
        "status": "pending_approval",
        "preflight_workflow_id": "wf_preflight_9a7d2f",
        "originating_agent_task_id": "task_agent_b7c8d9",
        "originating_agent_task": {
          "id": "task_agent_b7c8d9",
          "status": "completed",
          "agent_type": "preflight"
        }
      }
    }
    ```

    The full set of IPA statuses:

    * `qualifying` — preflight is validating the intent and scopes
    * `searching` — the agent is actively crawling for candidates
    * `validating` — candidates are being scored against constraints
    * `pending_clarification` — the agent needs more information from the user
    * `pending_approval` — approved candidate(s) awaiting user sign-off
    * `approved` — the user approved; the executing agent may proceed
    * `monitoring` — watching for trigger conditions (price, availability)
    * `executing` — the purchase is being executed
    * `completed` — the IPA fulfilled its intent
    * `failed` — terminal failure
    * `expired` — lifetime elapsed without fulfilment
    * `cancelled` — the user cancelled
  </Step>

  <Step title="Respond to clarifications (if needed)">
    If the agent lands in `pending_clarification`, answer the outstanding questions. The IPA returns to `qualifying` while the agent incorporates the answers.

    [`PUT /v0/user/ipa/{ipa_id}/answers`](/api-reference/ipa/clarify-ipa) — Submit clarification answers.

    ```bash theme={null}
    curl -X PUT https://api.sumvin.com/v0/user/ipa/ipa_1a2b3c4d/answers \
      -H "x-juno-jwt: <your-jwt-token>" \
      -H "x-juno-orgid: <your-org-id>" \
      -H "Content-Type: application/json" \
      -d '{
        "answers": {
          "preferred_color": "white",
          "new_or_used": "new only"
        }
      }'
    ```

    **Response:** `200 OK` with `intent.status: "qualifying"`.
  </Step>

  <Step title="Approve the IPA">
    This is the working artefact. Once the `IPA` record is `pending_approval`, approve it — the executing agent (the Envoy) can then begin searching under the terms you set.

    Approving authorises spending, so it is signed by the account holder rather than simply asserted. Fetch the prepared payload with `?expand=mandate`, sign `intent.approval_payload` with the wallet on the account (`eth_signTypedData_v4`), and send the signature with the decision. The spend ceiling, conditions and deadline are read from that signed payload, so an approval always means exactly what was signed.

    <CodeGroup>
      ```bash curl theme={null}
      # 1. Fetch the payload prepared for this Errand
      curl "https://api.sumvin.com/v0/user/ipa/ipa_1a2b3c4d?expand=mandate" \
        -H "x-juno-jwt: <your-jwt-token>" \
        -H "x-juno-orgid: <your-org-id>"

      # 2. Sign intent.approval_payload with the account's wallet, then send it back
      curl -X PUT https://api.sumvin.com/v0/user/ipa/ipa_1a2b3c4d/decision \
        -H "x-juno-jwt: <your-jwt-token>" \
        -H "x-juno-orgid: <your-org-id>" \
        -H "Content-Type: application/json" \
        -d '{ "decision": "approved", "signature": "0x<65-byte-signature>" }'
      ```

      ```typescript TypeScript theme={null}
      const headers = {
        "x-juno-jwt": process.env.SUMVIN_JWT!,
        "x-juno-orgid": process.env.SUMVIN_ORG_ID!,
        "Content-Type": "application/json",
      };

      // 1. Fetch the payload prepared for this Errand
      const prepared = await fetch(
        "https://api.sumvin.com/v0/user/ipa/ipa_1a2b3c4d?expand=mandate",
        { headers },
      );
      const { intent: pending } = await prepared.json();

      // 2. Sign it with the wallet on the account
      const signature = await walletClient.request({
        method: "eth_signTypedData_v4",
        params: [account, JSON.stringify(pending.approval_payload)],
      });

      // 3. Send the decision and the signature that authorises it
      const res = await fetch(
        "https://api.sumvin.com/v0/user/ipa/ipa_1a2b3c4d/decision",
        {
          method: "PUT",
          headers,
          body: JSON.stringify({ decision: "approved", signature }),
        },
      );
      const { intent } = await res.json();
      ```

      ```python Python theme={null}
      import httpx
      import os

      headers = {
          "x-juno-jwt": os.environ["SUMVIN_JWT"],
          "x-juno-orgid": os.environ["SUMVIN_ORG_ID"],
      }

      # 1. Fetch the payload prepared for this Errand
      prepared = httpx.get(
          "https://api.sumvin.com/v0/user/ipa/ipa_1a2b3c4d",
          params={"expand": "mandate"},
          headers=headers,
      )
      approval_payload = prepared.json()["intent"]["approval_payload"]

      # 2. Sign approval_payload with the account's wallet (eth_signTypedData_v4),
      #    then send the decision and the signature that authorises it
      res = httpx.put(
          "https://api.sumvin.com/v0/user/ipa/ipa_1a2b3c4d/decision",
          headers=headers,
          json={"decision": "approved", "signature": signature},
      )
      intent = res.json()["intent"]
      ```

      ```tsx React theme={null}
      import { useState } from 'react';

      const headers = {
        'x-juno-jwt': jwt,
        'x-juno-orgid': orgId,
        'Content-Type': 'application/json',
      };

      function ApproveIPAButton({ intentId }: { intentId: string }) {
        const [isPending, setIsPending] = useState(false);

        const handleApprove = async () => {
          setIsPending(true);
          try {
            // 1. Fetch the payload prepared for this Errand
            const prepared = await fetch(
              `https://api.sumvin.com/v0/user/ipa/${intentId}?expand=mandate`,
              { headers },
            );
            const { intent } = await prepared.json();

            // 2. The button authorises a purchase, so it signs one — the payload
            //    comes from the server and is signed as-is.
            const signature = await walletClient.request({
              method: 'eth_signTypedData_v4',
              params: [account, JSON.stringify(intent.approval_payload)],
            });

            // 3. Send the decision and the signature that authorises it
            await fetch(`https://api.sumvin.com/v0/user/ipa/${intentId}/decision`, {
              method: 'PUT',
              headers,
              body: JSON.stringify({ decision: 'approved', signature }),
            });
          } finally {
            setIsPending(false);
          }
        };

        return <button onClick={handleApprove} disabled={isPending}>Approve</button>;
      }
      ```
    </CodeGroup>

    <Note>
      The `@sumvin/api-hooks` package does not expose the signed approval yet — `useApproveIPA` still submits a decision without a signature, so it cannot approve. Call the endpoint directly, as above, until the hook is updated.
    </Note>

    <Tip>
      Using TanStack Query? Approving changes the Errand, so invalidate both `ipas.all` and `ipas.detail(id)` afterwards — see [invalidation strategy](/guides/tanstack-query-patterns#invalidation-strategy).
    </Tip>

    **Response:** `200 OK`

    ```json theme={null}
    {
      "intent": {
        "id": "ipa_1a2b3c4d",
        "status": "approved",
        "executing_agent_task_id": "task_agent_exec_3f4e5d"
      },
      "_links": {
        "self": { "href": "/v0/user/ipa/ipa_1a2b3c4d" },
        "cancel": { "href": "/v0/user/ipa/ipa_1a2b3c4d", "method": "DELETE" }
      }
    }
    ```

    <Check>The Errand is live. The executing agent has begun work under the scopes, constraints, and autonomy level you defined — and the user can revoke at any time.</Check>
  </Step>

  <Step title="Cancel at any time">
    Users can cancel an Errand at any non-terminal state. The executing agent halts and the `IPA` record transitions to `cancelled`.

    [`DELETE /v0/user/ipa/{ipa_id}`](/api-reference/ipa/cancel-ipa) — Cancel an Errand.

    ```bash theme={null}
    curl -X DELETE https://api.sumvin.com/v0/user/ipa/ipa_1a2b3c4d \
      -H "x-juno-jwt: <your-jwt-token>" \
      -H "x-juno-orgid: <your-org-id>"
    ```

    **Response:** `200 OK` with `intent.status: "cancelled"`.
  </Step>
</Steps>

## What's next

| Next                 | Where                                                 | When                                                              |
| -------------------- | ----------------------------------------------------- | ----------------------------------------------------------------- |
| Errand reference     | [Errand guide](/guides/errand)                        | Full field-by-field reference for constraints and autonomy levels |
| State machine        | [Errand lifecycle](/products/errand/errand-lifecycle) | When debugging a stuck status or wiring approval UI               |
| Scopes               | [Scopes](/identity/scopes)                            | Picking which Sigil capability strings the Errand carries         |
| Stamped Mandate mint | [Stamped Mandates](/identity/pint)                    | Before the Errand — the primitive Errands ride on                 |
