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

# Submit a transaction to Stellar RPC

> Use a looping mechanism to submit a transaction to the RPC.

This guide builds `submitTx`, a helper that sends a signed transaction to Stellar RPC and waits for the result. Other guides reuse it.

## Send statuses

[`sendTransaction`](https://developers.stellar.org/docs/data/apis/rpc/api-reference/methods/sendTransaction) returns right away with one of four statuses. Only `ERROR` is a final rejection.

| Status | Meaning | What to do |
| - | - | - |
| `PENDING` | The network accepted the transaction. It waits for a ledger. | Poll `getTransaction` for the result. |
| `DUPLICATE` | The network already has this transaction in its queue, for example from an earlier send. | Do not treat it as an error. Poll `getTransaction` for the result. |
| `TRY_AGAIN_LATER` | The network did not accept the transaction this time. Usually another transaction from the same source account still waits for a ledger. If you send a transaction again after it is in a ledger, you also get `TRY_AGAIN_LATER` for about 10 ledgers. | Wait, then send the same signed transaction again. |
| `ERROR` | The network rejected the transaction. `errorResult` has the reason. | Fix the transaction. But first call `getTransaction`: if you send a transaction again after it is in a ledger, you get `ERROR` (`txBAD_SEQ`) once those 10 or so ledgers pass. |

For more about these statuses and the errors behind them, see [Debugging errors](https://developers.stellar.org/docs/learn/fundamentals/contract-development/errors-and-debugging/debugging-errors.md).

## Wait for the result

[`getTransaction`](https://developers.stellar.org/docs/data/apis/rpc/api-reference/methods/getTransaction) returns `NOT_FOUND` until the transaction is in a ledger. `NOT_FOUND` does not mean that the transaction failed. The transaction can go into a ledger at any time until its time bound (`maxTime`) passes.

Poll until `getTransaction` returns `SUCCESS` or `FAILED`. If it still returns `NOT_FOUND` after a ledger closes later than `maxTime`, the transaction can never go into a ledger. Only then is it safe to build and send a new transaction.

> [!WARNING]
>
> `server.pollTransaction()` stops after a fixed number of attempts (30 by default) and then returns the last `NOT_FOUND` response. Do not treat that response as a failure. If you send a new transaction at that point, the network can apply both transactions, and you pay twice.

## The `submitTx` helper

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { FeeBumpTransaction, Transaction } from "@stellar/stellar-sdk";
import { Api, Server } from "@stellar/stellar-sdk/rpc";

const RPC_SERVER = "https://soroban-testnet.stellar.org/";
const server = new Server(RPC_SERVER);

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

// Sends a transaction, then waits until it is in a ledger or has expired.
async function submitTx(
  tx: Transaction | FeeBumpTransaction,
): Promise<Api.GetTransactionResponse> {
  // A transaction can go into a ledger only until its time bound (maxTime).
  const inner = tx instanceof FeeBumpTransaction ? tx.innerTransaction : tx;
  const maxTime = Number(inner.timeBounds?.maxTime ?? 0);
  if (maxTime === 0) {
    throw new Error("Call setTimeout() so that the transaction can expire");
  }

  // TRY_AGAIN_LATER: send the same signed transaction again after a pause.
  let reply = await server.sendTransaction(tx);
  while (
    reply.status === "TRY_AGAIN_LATER" &&
    Number(reply.latestLedgerCloseTime) <= maxTime
  ) {
    await sleep(2000);
    reply = await server.sendTransaction(tx);
  }

  // ERROR: a transaction that is already in a ledger also returns ERROR
  // (txBAD_SEQ) when you send it again, so look it up before you throw.
  if (reply.status === "ERROR") {
    const known = await server.getTransaction(reply.hash);
    if (known.status === Api.GetTransactionStatus.NOT_FOUND) {
      throw reply;
    }
  }

  // Poll until the transaction is in a ledger or has expired.
  // NOT_FOUND means "not in a ledger yet", not "failed".
  while (true) {
    const result = await server.getTransaction(reply.hash);
    switch (result.status) {
      case Api.GetTransactionStatus.SUCCESS:
        return result;
      case Api.GetTransactionStatus.FAILED:
        throw result;
      case Api.GetTransactionStatus.NOT_FOUND:
        // A ledger closed after maxTime, so the transaction can never go
        // into a ledger. Only now is it safe to build a new transaction.
        if (Number(result.latestLedgerCloseTime) > maxTime) {
          throw new Error(`Transaction ${reply.hash} expired`);
        }
        await sleep(1000);
    }
  }
}
```

RPC sends `latestLedgerCloseTime` as a string, so the helper converts it with `Number()` before it compares it with `maxTime`.

> [!WARNING]
>
> Remember: You should always handle errors gracefully! This is a fail-hard and fail-fast approach that should only be used in these examples.
