# Intents and retries

[API Documentation](https://docs.catena.com/api/get-intent.md)

Every governed action your agent takes is an intent. When your agent moves
money, creates a [counterparty](https://docs.catena.com/counterparties.md), asks a counterparty for its
payment details, or requests a
[temporary limit increase](https://docs.catena.com/policies.md#temporary-overrides) (the console calls
these temporary grants), we return an intent that records the action and its
outcome. Reads, such as balances, transactions, and deposit addresses, do not
return an intent.

An intent can pass through more than one status before it reaches a final one,
so we recommend that your agent read the status every time instead of assuming
the outcome.

## Statuses and resolutions

Use `status` for control flow. Below are the five statuses that an intent may
have, along with the matching `resolution`:

| `status`     | `resolution`                        | Meaning                                                                                          |
| ------------ | ----------------------------------- | ------------------------------------------------------------------------------------------------ |
| `completed`  | `completed`                         | The action has succeeded.                                                                        |
| `pending`    | `null`                              | The intent is waiting for a person to approve it.                                                |
| `processing` | `null`                              | Catena has accepted the intent, and it is in progress.                                           |
| `blocked`    | `blocked`, `denied`, or `cancelled` | A policy has blocked it, a person has denied it, or it has been cancelled. This status is final. |
| `failed`     | `failed` or `expired`               | The intent did not complete. This status is final.                                               |

`resolution` is the final outcome, and it is `null` while the intent is open. A
policy block and a denial both have the status `blocked`, so you can use
`resolution` to tell them apart (a cancelled intent has
`resolution: "cancelled"`). We may add new resolution values, so pass through
any value you don't recognize.

**Only `completed` means success.** If an intent is `pending` or `processing`,
read it again until it reaches a final status:

```sh
catena intents get int_...
```

## Intent fields

In addition to `status` and `resolution`, an intent carries the following fields
that explain its result:

| Field              | Contents                                                                                                                              |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
| `reasons`          | The policy decision or the approval route. Your agent should summarize it for the user.                                               |
| `expiresAt`        | The approval deadline for a pending intent.                                                                                           |
| `metadata.dataUrl` | A link to the approval or the result in the console. Give it to the person who approves.                                              |
| `data`             | The result, once one exists: the transaction for a send or transfer, or the new counterparty for a creation. It is `null` until then. |

To list your agent's recent intents, you can filter by status, type, and start
time (e.g., `--status pending`):

```sh
catena intents list --status pending --status processing
catena intents list --type send --start 2026-10-01T00:00:00Z
```

## Exit codes

`catena send` and `catena transfer` exit 0 as soon as they submit the intent,
even when the intent is blocked, pending, or later fails. This means a
successful exit does not tell you whether money moved, so read `.status` and
`.reasons` every time.

## Cancel an intent

An agent can cancel its own intent while the intent is waiting for approval, or
after approval but before execution starts:

```sh
catena intents cancel int_...
```

The intent will end with `resolution: "cancelled"`, and cancelling twice is
safe. However, an intent that is already executing cannot be cancelled, and the
API will return 409 (`intent_not_cancellable`).

## Retry safely

By default, each request waits up to 80 seconds. If a request times out, that
means the client stopped waiting, but the server may still have acted on the
request.

Reads are safe to run again. Intents are different, because an intent needs an
idempotency key to be retried safely. The CLI and the SDK generate a new key for
every call unless you pass one, so sending identical inputs twice will not mark
the second request as a retry.

### Example: retrying an ACH payment

Let's say Cora's Coffee's agent is paying Pete's Packaging $125.00 by ACH for
invoice 4471. To be able to retry, the agent chooses a key (here,
`7c4f2a8e-invoice-4471`) and saves it before the first attempt:

```sh
catena send --rail cpr_... --amount 125.00 --method ach \
  --idempotency-key 7c4f2a8e-invoice-4471
```

If the request times out, the agent runs the same command with the same key, and
no money will move a second time. Below is how we handle a repeated key, and
when to use a new one:

| Case                       | What happens                                                                                                                                                                                      |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Same key, same inputs      | We return the original intent, with `replayed: true`. No money moves a second time.                                                                                                               |
| Same key, different inputs | We return 409.                                                                                                                                                                                    |
| The policy changed         | A retry keeps the original source account, even if the policy changed.                                                                                                                            |
| A block was cleared        | After an admin clears a block (by changing the policy or approving an override), send again with a new key. The blocked intent stays blocked and moved no money, so the new key cannot pay twice. |
| A new operation            | Use a new key for a new operation, or when an intent `failed` and another attempt is correct.                                                                                                     |

> [!WARNING] **Completed intents**
>
> Don't pay a `completed` intent again to recover from an error.

### Payment errors and retries

The following messages can come back from a payment, and each one calls for a
different response:

| Message                                                     | What to do                                                                                      |
| ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| "Already prepared" (409 `wallet_send_intent_not_executing`) | Run `intents get <id>` if you have the ID. Otherwise ask an admin. Do not retry with a new key. |
| "Created but never submitted; no money moved."              | Fix the problem in the message, then use a new key.                                             |
| "Could not be confirmed."                                   | Read `intents get <id>` until it is final. Submit again only after `blocked` or `failed`.       |

## Using the SDK

In the SDK, `submitIntent` returns the intent's current state (it does not wait
for execution). You can poll with `getIntent(id)`, cancel with
`cancelIntent(id)`, and pass `idempotencyKey` yourself when a retry must refer
to the same operation.

```ts
const intent = await client.submitIntent({
  action: { type: "send", counterpartyRailId, amount: "125.00", method: "ach" },
  idempotencyKey,
})
if (intent.status === "pending") {
  // Waiting for a person. intent.reasons says why.
}
```

## Related

- [Handle approvals](https://docs.catena.com/approvals.md): Learn what goes to the approvals queue, who can approve, when requests expire, and what happens between an approval and a completed payment.
- [Test in the sandbox](https://docs.catena.com/test-in-sandbox.md): The policy rules and requests that produce each payment outcome on test money, and what your agent will see for each one.
- [Troubleshooting](https://docs.catena.com/troubleshooting.md): The errors you may run into while linking, signing, and sending payments, with the cause of each and what to do about it.
- [Get an intent](https://docs.catena.com/api/get-intent.md): Get an intent by id.
