Sending transactions
Send a signed transaction and get its final effects, events and balance changes in the same call, as soon as validators certify it. No polling, no second request.
Overview
You build and sign the transaction yourself, with any SDK or wallet. tide takes the signed bytes, hands them from its own full node straight to Sui validators and answers once a quorum of validators has certified the effects. Certified effects are final: the result can no longer change. tide does not wait for the transaction to land in a checkpoint, which usually follows within a second.
- Transactions that use only your own objects (transfers, splits, merges, most wallet actions) take the fast path and need no consensus round. Transactions with shared objects (DEX swaps, most DeFi) go through consensus.
- A transaction that runs and fails, for example on a Move abort, is still final and still costs gas. It comes back as a normal answer with status failure and the reason.
- A transaction the network refuses before running it (a bad signature, too little gas, an object another transaction already used) comes back as an error with a reason you can act on. Nothing changes on chain and no gas is charged.
- Sending the same bytes again is always safe. It returns the same result and never runs twice. See Resending is safe.
Quickstart
Get a key on API keys (the Free plan works). This example uses the Sui TypeScript SDK to build and sign, and tide to send.
import { SuiGrpcClient } from "@mysten/sui/grpc";
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
import { Transaction } from "@mysten/sui/transactions";
const sui = new SuiGrpcClient({ network: "mainnet", baseUrl: "https://fullnode.mainnet.sui.io:443" });
const keypair = Ed25519Keypair.fromSecretKey(process.env.SUI_PRIVATE_KEY!);
// 1. Build and sign locally. Your key never leaves your machine.
const tx = new Transaction();
tx.setSender(keypair.toSuiAddress());
const [coin] = tx.splitCoins(tx.gas, [1_000_000]); // 0.001 SUI
tx.transferObjects([coin], "0xRECIPIENT_ADDRESS");
const { bytes, signature } = await tx.sign({ client: sui, signer: keypair });
// 2. Send through tide. The answer arrives once the effects are final.
const res = await fetch("https://api.tide.ad/v0/transactions/send", {
method: "POST",
headers: { "content-type": "application/json", "x-api-key": "YOUR_API_KEY" },
body: JSON.stringify({ txBytes: bytes, signatures: [signature] }),
}).then((r) => r.json());
if (res.error) throw new Error(`${res.error.code}: ${res.error.message}`);
console.log(res.digest, res.status, `final in ${res.timing.finalMs} ms`);
for (const b of res.balanceChanges) console.log(b.address, b.coinType, b.amount);With curl, given base64 bytes and a signature:
curl -X POST "https://api.tide.ad/v0/transactions/send" \
-H "x-api-key: YOUR_API_KEY" -H "content-type: application/json" \
-d '{"txBytes":"AAACAAgA...","signatures":["AKe6ytTI..."]}'Existing JSON-RPC clients
sui_executeTransactionBlock on https://api.tide.ad/ takes the usual params (bytes, signatures, options, request type) and answers with the usual SuiTransactionBlockResponse, so a JSON-RPC client can send through tide unchanged. It always waits for certified effects; the request type is accepted and confirmedLocalExecution is false. showInput returns the sender, gas data and signatures only; use showRawInput for the full transaction.
import { SuiJsonRpcClient } from "@mysten/sui/jsonRpc";
const tide = new SuiJsonRpcClient({ url: "https://api.tide.ad/?api-key=YOUR_API_KEY", network: "mainnet" });
// bytes and signature from tx.sign(...) above
const result = await tide.executeTransactionBlock({
transactionBlock: bytes,
signature,
options: { showEffects: true, showEvents: true, showObjectChanges: true, showBalanceChanges: true },
});
console.log(result.digest, result.effects?.status.status);curl -X POST "https://api.tide.ad/?api-key=YOUR_API_KEY" -H "content-type: application/json" -d '{
"jsonrpc": "2.0", "id": 1, "method": "sui_executeTransactionBlock",
"params": ["AAACAAgA...", ["AKe6ytTI..."], { "showEffects": true, "showEvents": true }, "WaitForLocalExecution"]
}'Request
POST /v0/transactions/send with a JSON body:
| Field | Type | What |
|---|---|---|
| txBytes | string | The transaction the wallet signed: base64 BCS TransactionData. This is the bytes value that tx.sign() and wallets' signTransaction return. |
| signatures | string[] | Base64 serialized signatures, one per signer: the sender's, plus the sponsor's when someone else pays gas. A multisig, zkLogin or passkey signature counts as one. A single string is accepted too. |
Response
| Field | What |
|---|---|
| digest | The transaction digest. |
| status | success or failure. Both are final. |
| error | null on success. On failure: message, command (the index of the command that failed) and, for a Move abort, abort with package, module, function, instruction, code and constant (the error constant's name when the module has one). |
| final | Always true: a quorum of validators certified these effects. |
| sender, kind | The sender, and owned (only the sender's own objects) or shared (uses shared objects). |
| checkpoint | null in the answer: the transaction is final before it is in a checkpoint. A second later, getTransaction or POST /v0/transactions has the checkpoint and the parsed transaction. |
| gas | computationCost, storageCost, storageRebate, nonRefundableStorageFee and totalMist (computation plus storage minus rebate), in MIST as strings. |
| balanceChanges | { address, coinType, amount } for every coin balance that changed; amount is signed, in the coin's smallest unit. |
| objects | created, mutated, unwrapped, deleted, wrapped and published, each with objectId, version, digest, owner and type where they apply. |
| events | { type, packageId, module, sender, json, bcs } in the order the transaction emitted them. |
| effects | digest, epoch, lamportVersion, eventsDigest, dependencies and bcs (base64 TransactionEffects, for SDKs that parse effects themselves). |
| resent | true when tide already had this digest from an earlier send and answered with that result. |
| timing.finalMs | Milliseconds from tide receiving the request to final effects. |
Owners use the Sui JSON form: { AddressOwner }, { ObjectOwner }, { Shared: { initial_shared_version } }, "Immutable" or { ConsensusAddressOwner: { start_version, owner } }. Trim the answer with fields= like any call, for example ?fields=digest,status,error,timing.
Resending is safe
A Sui transaction is identified by its digest, and the network runs a digest at most once. tide keys every send by digest too: a second send of the same bytes while the first is in flight waits for it, and a send of bytes that already landed returns the same effects with resent: true. So when a request times out or a connection drops, send the same bytes again. Do not rebuild the transaction to retry: a rebuilt transaction has a new digest.
If no final effects arrive within 30 seconds the answer is upstream_busy with the digest. The transaction may still land: send the same bytes again, or look it up by digest with getTransaction.
Errors
Errors use the usual shape (see Errors). A refusal before execution is rejected (HTTP 422, JSON-RPC -32002) with data.digest, data.reason and data.nodeMessage, the network's own words.
| reason | What to do |
|---|---|
| SIGNATURE_INVALID | A signature does not verify for these bytes. Sign the exact bytes you send, with the sender's key, and add the sponsor's signature when someone else pays gas. |
| GAS_BALANCE_TOO_LOW | The gas coins hold less SUI than the budget. Use more gas coins or lower the budget. |
| GAS_BUDGET_TOO_LOW | Raise the budget to at least the minimum and sign again. |
| GAS_BUDGET_TOO_HIGH | The budget is above 50 SUI. Lower it and sign again. |
| GAS_PRICE_TOO_LOW | Set at least this epoch's reference gas price and sign again. |
| OBJECT_LOCKED | Another transaction already uses one of these object versions. Wait for it to finish, then rebuild with the new versions. |
| OBJECT_VERSION_UNAVAILABLE | An input object changed since you built the transaction. Rebuild with current versions and sign again. |
| OBJECT_NOT_FOUND | An input object does not exist (wrong id, deleted or wrapped). |
| EXPIRED | The transaction's expiration has passed. Build it again. |
| ADDRESS_DENIED | An address is on the deny list of a regulated coin the transaction uses. |
| TOO_LARGE | The transaction is over a protocol size limit. Split it. |
| REJECTED | Anything else; nodeMessage says what. |
invalid_params (400) means the body itself is wrong: bytes that are not base64 TransactionData, or no signatures. upstream_busy (503) with reason OVERLOADED means validators were busy with the objects the transaction uses; send the same bytes again after retry-after.
Credits and rate limits
A send costs 5 credits; an answer from an earlier send of the same digest costs 1. Refusals and errors cost nothing. Sends have their own rate class, at your plan's heavy rate, and their own pool on tide's side, so a burst of sends never waits behind other calls.
| Plan | Sends per second | Sends a month in plan credits |
|---|---|---|
| No key | 2 (shared with all calls) | 200 a day per IP |
| Free | 5 | 1,000,000 |
| Developer | 25 | 6,000,000 |
| Business | 100 | 50,000,000 |
| Professional | 250 | 120,000,000 |
See Plans and pricing.
Methods
| Method | What it does | Credits |
|---|---|---|
| tx.sendPOST /v0/transactions/send | Send a signed transaction and get its final effects, events and balance changes. | 5 |
| sui_executeTransactionBlockJSON-RPC | Execute a signed transaction (Sui JSON-RPC compatible) and answer with final effects. | 5 |