# Send base64 text

Send a signed transaction or bundle as base64 text over HTTP, using the same payload envelope as the raw-byte route.

Source: https://docs.zerocopy.sh/api/http/plaintext

`POST https://sol.zerocopy.sh/plaintext`

Authentication: API key, one of: `x-api-key` header, `api-key` query, `Authorization: Bearer` header.

## Request body (`text/plain`)

The same [payload](/api/payload-envelope) as [/binary](/api/http/binary), encoded as base64 text. Maximum body 21,888 bytes; decoded payload at most 16,403 bytes, received within 2 seconds. There is no encoding option or preflight simulation. The same [tip](/sending/tips), [send rate](/sending/limits#send-rate) and transaction rules apply as for JSON-RPC. Uses the same hosts, ports and [authentication](/connect/authentication) as JSON-RPC.

```text title="Base64-encoded signed transaction payload"
BASE64_PAYLOAD
```

## Responses

### 200

Returns the transaction's first signature in base58 or the [bundle id](/api/send-bundle) as 64 hex characters, in plain text. Acceptance does not mean landing; confirm signatures through your Solana RPC provider.

```text title="Signature"
BASE58_SIGNATURE
```

```text title="Bundle id"
0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
```

### 400

JSON rejection body, with the reason, its fields, `message`, `retryable` and `docs`, as in JSON-RPC [`error.data`](/api#responses).
- [`malformed`](/errors#malformed): invalid payload or transaction.
- [`unsigned`](/errors#unsigned): unsigned transaction.
- [`signature_count`](/errors#signature_count): signature mismatch.
- [`missing_tip`](/errors#missing_tip): tip below the minimum.
- [`credit_exhausted`](/errors#credit_exhausted): declared-tip credit unavailable.
- [`bundle_size`](/errors#bundle_size): invalid transaction count.
- [`bundle_control`](/errors#bundle_control): control-account rule broken.
- [`invalid_request`](/errors#invalid_request): other than `not_found` or `body_timeout`.

```json title="missing_tip"
{
  "reason": "missing_tip",
  "minimum": 1000000,
  "found": 123,
  "message": "tip too small: the tip transfers add up to 123 lamports, the minimum is 1,000,000; raise the transfer to one of the tip accounts to at least 1,000,000 lamports",
  "retryable": false,
  "docs": "https://docs.zerocopy.sh/errors#missing_tip"
}
```

### 401

[`unauthorized`](/errors#unauthorized): missing, unknown or revoked API key.

```json title="unauthorized"
{
  "reason": "unauthorized",
  "message": "API key missing or not recognized; pass it as the api-key query parameter, an x-api-key header or Authorization: Bearer",
  "retryable": false,
  "docs": "https://docs.zerocopy.sh/errors#unauthorized"
}
```

### 402

[`payment_required`](/errors#payment_required): sending is suspended until the overdue invoice is paid; `unpaid_lamports` is outstanding.

```json title="payment_required"
{
  "reason": "payment_required",
  "unpaid_lamports": 1000000,
  "message": "The key is suspended until its overdue invoice is paid.",
  "retryable": false,
  "docs": "https://docs.zerocopy.sh/errors#payment_required"
}
```

### 403

[`unsupported`](/errors#unsupported): `feature: bundles`; bundles are not enabled for this key.

```json title="unsupported"
{
  "reason": "unsupported",
  "feature": "bundles",
  "message": "bundles are not enabled for this key; ask ZeroCopy to enable them",
  "retryable": false,
  "docs": "https://docs.zerocopy.sh/errors#unsupported"
}
```

### 404

GET is supported only at [/health](/api/http/health) and [/tip_floor](/api/http/tip-floor); methods other than GET and POST are refused. POST to another path is [JSON-RPC](/api).
- [`invalid_request`](/errors#invalid_request): `problem: not_found`.
- [`unsupported`](/errors#unsupported): `feature: tip_floor` from the agent.

```json title="invalid_request"
{
  "reason": "invalid_request",
  "problem": "not_found",
  "message": "unknown path or method: POST raw transactions to /binary, base64 to /plaintext, or JSON-RPC to any other path",
  "retryable": false,
  "docs": "https://docs.zerocopy.sh/errors#invalid_request"
}
```

### 408

[`invalid_request`](/errors#invalid_request): `problem: body_timeout`; the body did not arrive within 2 seconds.

```json title="invalid_request"
{
  "reason": "invalid_request",
  "problem": "body_timeout",
  "message": "the request body did not arrive within 2 seconds; retry",
  "retryable": true,
  "docs": "https://docs.zerocopy.sh/errors#invalid_request"
}
```

### 413

[`too_large`](/errors#too_large): request body, decoded payload or transaction exceeds a size limit.

```json title="too_large"
{
  "reason": "too_large",
  "limit": 16403,
  "size": 16404,
  "message": "The payload exceeds the byte limit.",
  "retryable": false,
  "docs": "https://docs.zerocopy.sh/errors#too_large"
}
```

### 429

[`rate_limited`](/errors#rate_limited): over the send rate, with `per_second` and `retry_after_ms`, or a repeat of a recent submission, without numbers.

```json title="rate_limited"
{
  "reason": "rate_limited",
  "limit": "transactions",
  "message": "rate limited: too many transactions from this key; retry shortly",
  "retryable": true,
  "docs": "https://docs.zerocopy.sh/errors#rate_limited"
}
```

### 503

JSON rejection body; submission is temporarily unavailable.
- [`not_ready`](/errors#not_ready): edge not ready.
- [`overloaded`](/errors#overloaded): edge temporarily busy.
- [`forward_failed`](/errors#forward_failed): transaction could not be dispatched.
- [`preflight_unavailable`](/errors#preflight_unavailable): simulation unavailable.
- [`internal`](/errors#internal): internal error.

```json title="not_ready"
{
  "reason": "not_ready",
  "waiting_for": "leader_route",
  "message": "the region is not ready to send yet; retry in a moment",
  "retryable": true,
  "docs": "https://docs.zerocopy.sh/errors#not_ready"
}
```

## Code samples

```ts title="TypeScript (payload encoder)"
// payload.ts
export function transactionPayload(
  transaction: Uint8Array,
  declaredTip?: bigint,
): Uint8Array<ArrayBuffer> {
  if (declaredTip === undefined) return concat([transaction])
  return concat([Uint8Array.of(0x00, 0x01), u64(declaredTip), transaction])
}

export function bundlePayload(
  transactions: Uint8Array[],
  declaredTip?: bigint,
): Uint8Array<ArrayBuffer> {
  if (transactions.length < 1 || transactions.length > 4) {
    throw new RangeError('a bundle holds 1 to 4 transactions')
  }
  const parts: Uint8Array[] = [Uint8Array.of(0x00, declaredTip === undefined ? 0x02 : 0x03)]
  if (declaredTip !== undefined) parts.push(u64(declaredTip))
  parts.push(Uint8Array.of(transactions.length))
  for (const transaction of transactions) parts.push(u16(transaction.length), transaction)
  return concat(parts)
}

function u64(value: bigint): Uint8Array<ArrayBuffer> {
  const bytes = new Uint8Array(8)
  new DataView(bytes.buffer).setBigUint64(0, value, true)
  return bytes
}

function u16(value: number): Uint8Array<ArrayBuffer> {
  const bytes = new Uint8Array(2)
  new DataView(bytes.buffer).setUint16(0, value, true)
  return bytes
}

function concat(parts: Uint8Array[]): Uint8Array<ArrayBuffer> {
  const out = new Uint8Array(parts.reduce((size, part) => size + part.length, 0))
  let offset = 0
  for (const part of parts) {
    out.set(part, offset)
    offset += part.length
  }
  return out
}
```

```rust title="Rust (payload encoder)"
// Uses the client helper from the API overview (/api#client-helper).
// src/payload.rs
use crate::swqos::Result;

pub fn transaction_payload(transaction: &[u8], declared_tip: Option<u64>) -> Vec<u8> {
    let mut payload = Vec::with_capacity(
        transaction.len() + usize::from(declared_tip.is_some()) * 10,
    );
    if let Some(tip) = declared_tip {
        payload.extend_from_slice(&[0x00, 0x01]);
        payload.extend_from_slice(&tip.to_le_bytes());
    }
    payload.extend_from_slice(transaction);
    payload
}

pub fn bundle_payload(
    transactions: &[Vec<u8>],
    declared_tip: Option<u64>,
) -> Result<Vec<u8>> {
    if !(1..=4).contains(&transactions.len()) {
        return Err("a bundle holds 1 to 4 transactions".into());
    }
    let size = 3
        + usize::from(declared_tip.is_some()) * 8
        + transactions
            .iter()
            .map(|transaction| 2 + transaction.len())
            .sum::<usize>();
    let mut payload = Vec::with_capacity(size);
    payload
        .extend_from_slice(&[0x00, if declared_tip.is_some() { 0x03 } else { 0x02 }]);
    if let Some(tip) = declared_tip {
        payload.extend_from_slice(&tip.to_le_bytes());
    }
    payload.push(transactions.len() as u8);
    for transaction in transactions {
        payload.extend_from_slice(&(transaction.len() as u16).to_le_bytes());
        payload.extend_from_slice(transaction);
    }
    Ok(payload)
}
```

```go title="Go (payload encoder)"
// payload.go
package swqos

import (
	"encoding/binary"
	"errors"
)

func TransactionPayload(transaction []byte, declaredTip *uint64) []byte {
	if declaredTip == nil {
		return append([]byte(nil), transaction...)
	}
	payload := make([]byte, 10+len(transaction))
	payload[1] = 0x01
	binary.LittleEndian.PutUint64(payload[2:10], *declaredTip)
	copy(payload[10:], transaction)
	return payload
}

func BundlePayload(transactions [][]byte, declaredTip *uint64) ([]byte, error) {
	if len(transactions) < 1 || len(transactions) > 4 {
		return nil, errors.New("a bundle holds 1 to 4 transactions")
	}
	size := 3
	if declaredTip != nil {
		size += 8
	}
	for _, transaction := range transactions {
		size += 2 + len(transaction)
	}
	payload := make([]byte, size)
	payload[1] = 0x02
	offset := 2
	if declaredTip != nil {
		payload[1] = 0x03
		binary.LittleEndian.PutUint64(payload[offset:offset+8], *declaredTip)
		offset += 8
	}
	payload[offset] = byte(len(transactions))
	offset++
	for _, transaction := range transactions {
		binary.LittleEndian.PutUint16(payload[offset:offset+2], uint16(len(transaction)))
		offset += 2
		copy(payload[offset:], transaction)
		offset += len(transaction)
	}
	return payload, nil
}
```
