> For the complete documentation index, see [llms.txt](https://savefee.gitbook.io/savefee/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://savefee.gitbook.io/savefee/broadcast/broadcast-a-transaction.md).

# Broadcast a transaction

## Set the expiration first

{% hint style="danger" %}
**Set the transaction expiration to roughly 600 seconds before signing.**

TronWeb builds transactions with an expiration of about 60 seconds by default. SaveFee needs a window to provision the resource **and then** broadcast, so a short expiration is rejected. Nothing is charged, but nothing is delivered either.
{% endhint %}

How much time has to be left when the request arrives:

| Time left            | Result                            |
| -------------------- | --------------------------------- |
| Less than 30 seconds | `422 broadcast.tx_expired`        |
| 30 to 75 seconds     | `422 broadcast.insufficient_time` |
| 75 seconds or more   | Accepted                          |

75 seconds is the floor, not a target. Sign with \~600 seconds so queueing and network latency cannot eat the margin.

## Build and sign

Build the transaction however you normally would, extend its expiration, and sign it locally. The shape of the transaction is up to you — the steps are the same either way.

```javascript
// A contract call. A USDT transfer is the common case:
const built = await tronWeb.transactionBuilder.triggerSmartContract(
  USDT_CONTRACT,
  'transfer(address,uint256)',
  {},
  [
    { type: 'address', value: recipient },
    { type: 'uint256', value: amountAtomic },
  ],
  sender,
);
const tx = built.transaction;

// … or any other transaction, built the usual way:
// const tx = await tronWeb.transactionBuilder.sendTrx(recipient, amountSun, sender);

// Extend the window so SaveFee can provision the resource and then broadcast.
const extended = await tronWeb.transactionBuilder.extendExpiration(tx, 600);

const signedTx = await tronWeb.trx.sign(extended, PRIVATE_KEY);
```

Token amounts are in the token's smallest unit. USDT has 6 decimals, so `amountAtomic` for 1.5 USDT is `1500000`.

## Submit it

```
POST /v1/broadcasts
Authorization: Bearer sf_live_…
Content-Type: application/json
```

```json
{
  "signedTx": { "… the full signed transaction object …": null }
}
```

Send the signed transaction exactly as your TRON library produced it — the whole object, including `txID`, `raw_data`, `raw_data_hex` and `signature`. Do not reshape it.

{% hint style="warning" %}
**The key you send decides who pays.** With `Authorization`, the key's account is charged — even if the transaction was signed by another address. Without it, the signer's own SaveFee account is charged, and that address must already be onboarded.

Use the `Bearer` scheme exactly. Because authentication is optional on this endpoint, a header in any other scheme is read as no key at all and the signer is charged instead of you — see **Overview and flow**.

Check what you are signing before you sign it. An accepted transaction is broadcast to TRON and charged, and once it is on-chain it cannot be recalled.
{% endhint %}

## Accepted

```json
{
  "broadcastId": "65f1a2b3c4d5e6f708192a3b",
  "status": "pending_energy"
}
```

The status code is `202`, meaning *accepted for processing* — the transaction is not on-chain yet. The fee is already debited at this point; it is refunded if the broadcast ends in `failed` or `expired`.

Store `broadcastId` and poll it, or wait for the webhook. The `202` body does not repeat the transaction id: you already have it locally from signing, and it is also returned by `GET /v1/broadcasts/{broadcastId}` as `txId`. See **Track and troubleshoot**.

## Duplicate protection

A transaction is deduplicated by its **`txId`**, so `Idempotency-Key` is not needed here. Resending the same signed transaction while its broadcast is still being processed — or after it has confirmed on-chain — returns `409 broadcast.duplicate_tx`, which is what stops you being charged twice for one transaction.

Once a broadcast has ended in `failed` or `expired`, the `txId` is released and the same transaction is accepted again. That is the intended retry path, not an accident. See **Track and troubleshoot**.

## What is checked before anything is charged

Every check below runs **before** the transaction goes to the network, so a rejection at any of them costs you nothing:

* the body is a readable signed transaction — `400`
* the transaction carries a signature — `400`
* `txID` matches the transaction body — `422`
* the signature recovers to the declared sender — `422`
* the expiration leaves a provisioning window — `422`
* the transaction is not already expired — `422`
* simulation says the transaction would succeed — `422`
* the paying account exists and has enough balance — `422` / `402`
