# Send raw bytes

Send a signed transaction or bundle as raw payload bytes over HTTP, with an optional declared tip.

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

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

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

## Request body (`application/octet-stream`)

One signed transaction or a [payload envelope](/api/payload-envelope) with a declared tip or bundle. Maximum body 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="Signed transaction wire bytes"
SIGNED_TRANSACTION_BYTES
```

## 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
}
```

```ts title="TypeScript"
// Uses the client helper from the API overview (/api#client-helper).
import { readFile } from 'node:fs/promises'
import { bundlePayload } from './payload.ts'
import { required, url } from './swqos.ts'

// Signed transaction files, in bundle order.
const transactions = await Promise.all(
  process.argv.slice(2).map(async (file) => new Uint8Array(await readFile(file))),
)
const response = await fetch(`${url}/binary`, {
  method: 'POST',
  headers: { 'x-api-key': required('SWQOS_API_KEY') },
  body: bundlePayload(transactions),
})
const body = await response.text()
console.log(response.ok ? `bundle ${body}` : `refused ${response.status}: ${body}`)
```

```rust title="Rust"
// Uses the client helper from the API overview (/api#client-helper).
use swqos_examples::{
    payload::bundle_payload,
    swqos::{Result, required, url},
};

#[tokio::main]
async fn main() -> Result<()> {
    // Signed transaction files, in bundle order.
    let mut transactions = Vec::new();
    for file in std::env::args().skip(1) {
        transactions.push(tokio::fs::read(file).await?);
    }
    let response = reqwest::Client::new()
        .post(format!("{}/binary", url()))
        .header("x-api-key", required("SWQOS_API_KEY")?)
        .body(bundle_payload(&transactions, None)?)
        .send()
        .await?;
    let status = response.status();
    let body = response.text().await?;
    if status.is_success() {
        println!("bundle {body}");
    } else {
        println!("refused {}: {body}", status.as_u16());
    }
    Ok(())
}
```

```go title="Go"
// Uses the client helper from the API overview (/api#client-helper).
package swqos

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"net/http"
	"os"
)

func SendBinary(ctx context.Context) error {
	// Signed transaction files, in bundle order.
	var transactions [][]byte
	for _, file := range os.Args[1:] {
		transaction, err := os.ReadFile(file)
		if err != nil {
			return err
		}
		transactions = append(transactions, transaction)
	}
	payload, err := BundlePayload(transactions, nil)
	if err != nil {
		return err
	}
	apiKey, err := Required("SWQOS_API_KEY")
	if err != nil {
		return err
	}
	request, err := http.NewRequestWithContext(ctx, http.MethodPost, URL()+"/binary", bytes.NewReader(payload))
	if err != nil {
		return err
	}
	request.Header.Set("x-api-key", apiKey)
	response, err := http.DefaultClient.Do(request)
	if err != nil {
		return err
	}
	defer response.Body.Close()
	body, err := io.ReadAll(response.Body)
	if err != nil {
		return err
	}
	if response.StatusCode >= 200 && response.StatusCode < 300 {
		fmt.Printf("bundle %s\n", body)
	} else {
		fmt.Printf("refused %d: %s\n", response.StatusCode, body)
	}
	return nil
}
```
