Texel API documentation

Within the restrictions set out by our License & Terms of service, you may automate certain actions within our service. Where not explicitly clarified elsewhere, the presence of documentation for a specific feature or endpoint on this page implies permission to operate said feature in an automated manner, (such as via a custom program, periodically, or through a third party), anywhere in the world where it is permitted by law.

No guarantee is made on our part about the availability, uptime, or stability of any particular endpoint. The service as described herein is provided on a "best effort" basis. We are not responsible for any losses incurred by network issues, downtime, user error, protocol misuse, or otherwise.


Gateway & Authentication

Obtaining an API token

You can obtain an API token in the  "Devices & API Tokens"  section of the account panel. The current limit is 8 API tokens. If you create a new one, the oldest one will be automatically deleted. Note that when a token is removed, all currently connected clients using that token are instantly disconnected with error code 3003

While most APIs behave identically for API tokens and user tokens (ones obtained from localStorage.texel_token), it is strictly forbidden to use a user token in an automated manner for any endpoints that do not explicitly permit it. If we detect that you may be using a token type incorrectly, we will likely revoke the token immediately before issuing you a formal warning, or, in repeated cases, closing your account.

API Prefix

The default prefix for all API endpoints is

https://server.rplace.live:8443

This means websocket connections are made to wss://server.rplace.live:8443/...

For 3rd-party servers, the API endpoint prefix within the official client can be changed via localStorage.api_endpoint


WebSocket Gateway

WEBSOCKET /<token>

Connects to the WebSocket gateway for all in-game actions, such as subscribing to areas or placing pixels

All WebSocket endpoints are performed in binary. It is recommended that you use the nanobuf library as that is how the server serializes and parses messages. If you wish to implement serialization from scratch, you can find more details, as well as reference implementations, from one of the github links listed below UTF-8 messages from either the client or the server are non-normative (e.g debug or logging) and should generally be ignored (and not sent) in production.

When a connection is closed, all associated server resources are freed (You do not need to manually unsubscribe from chunks for example, and you are not recommended to)


Ratelimiting

Ratelimiting is calculated over a 6 hour window and shared between all API endpoints. The ratelimit applied for invoking a given endpoint may be higher if the endpoint fails for a reason that could have easily been avoided (e.g malformed or repeated request).

The "Ratelimit cost" for each endpoint describes the time cost of calling the endpoint if it succeeds. For example, an endpoint with a Ratelimit cost of 30 seconds, if called every 30 seconds, will use up 100% of the sender's allocated ratelimit. In this example, if the endpoint fails due to ratelimiting, you should wait at least 30 seconds without making any other requests before trying again.


Account management

GET /revoke/<token>

Ratelimit cost: 15 seconds

Revokes token, which is also used for authenticating the request. This endpoint may be used on user tokens as well as API tokens. Returns 1 on success and 0 on failure

const TOKEN = "dXNlckBleGFtcGxlLmNvbQ.FFjw3mC06aoCJtN4dN3dlwOFAKEToK3n"; fetch(ENDPOINT + "/revoke/" + TOKEN) .then(res => res.text()).then(result => { if(result == "1"){ console.log("Token deleted") } })

Subscribing to updates

All coordinates are x+ right and y+ up. Chunks are 256x256 pixels in size. The entire world is 256x256 chunks in size (and wraps around at x=±32768 and y=±32768). For clarity, absolute coordinates are denoted x,y, chunk coordinates (divided) are denoted cx,cy and chunk-local coordinates are denoted lx,ly.

Colors are represented as 16 bit integers in the RGB5 format, that is, r | g<<5 | b<<10 for 0 ≤ r,g,b < 32. The highest bit is currently ignored, thus, making a total of 32768 unique colors.

Subscribe to chunk(s)

C: v32(8) [ u8(cx) u8(cy) ]*

Subscribe to one or more chunk(s) specified by the array of chunk positions (cx, cy). Chunk positions and pixel coordinates are related via shift-by-8 operations (i.e chunk position for (x,y) is (x >> 8 & 0xff, y >> 8 & 0xff)). If this connection is already subscribed to one of the chunks provided, nothing happens for that chunk (i.e it is not resent).

Ratelimit cost: 1 second per 200 chunks

C: 08 00 00 01 00 00 FF

For example, this message subscribes to three chunks: (0, 0), (1, 0) and (0, -1)


Unsubscribe from chunk(s)

C: v32(9) [ u8(cx) u8(cy) ]*

Unsubscribe from one or more chunk(s) specified by the array of chunk positions (cx, cy). See above for more details. If this connection was not subscribed to one of the chunks provided, nothing happens for that chunk.


Metadata packet

S: v32(1) [ v32(tag) data... ]* v32(0)

This packet is sent immediately after a connection is opened and includes some basic information, as listed below.

Tag = 1. Account balance in units of $0.0001. If this tag is not present, assume the balance to be 0.

u64(account_balance)

Tag = 2. Account balance increase notification in units of $0.0001. This is used to indicate that the balance has increased by a certain amount since the notification was last cleared (this is used primarily by user-controlled clients and is a UX feature).

u64(account_notif_balance)

Tag = 0. This is used to signify the end of the tag-value section and the end of the packet.


Clear notification packet

C: v32(15) u64(by_amount)

Used to clear account balance increase notification. See above. The balance increase notification will be decreased by by_amount in units of $0.0001 and clamped to be no less than 0.


Incoming chunk data

S: v32(16) u8(cx) u8(cy) v32(appended) u8arr(color_data) u8arr(price_data) v32(owned_ranges_count) v32(of_which_currently_owned)
[ u32(owned_ranges) ]*owned_ranges_count

Incoming data for the chunk at (cx, cy). This packet is usually in response to a "Subscribe to chunk(s)" packet. Note that one request to subscribe to many chunks will be responded with many individual chunk data packets.

color_data is the concatenation of a zlib raw compressed chunk data and appended 32-bit integers (big endian). Uncompressed, the chunk data is 131072 bytes, i.e, 256*256 16-bit integers, corresponding to the color data for this chunk. The order is left-to-right, bottom-to-top. Each 32-bit integer from the append section changes one pixel in the uncompressed chunk data. The high 16 bits correspond to the index and the low 16 bits to the new value.

price_data is the concatenation of a zlib raw compressed chunk and appended 32-bit integers (big endian). Uncompressed, the chunk data is 262144 bytes, i.e, 256*256 32-bit integers, corresponding to price for each pixel in units of $0.0001. Each 32-bit integer from the append section changes one pixel in the uncompressed chunk data. The index is borrowed from the equivalent 32-bit integer in the color_data's append section, and the value to be replaced is the whole 32-bit integer from this append section.

append = n -------------------|--------------------------| v v [[ zlib color data ... ] [ append section: 4n bytes ]] [[ zlib price data ... ] [ append section: 4n bytes ]]

The list of owned_ranges represents a range of pixel indices that you either currently or previously owned. The first owned_ranges_count - of_which_currently_owned elements represent pixels previously owned, and the last of_which_currently_owned represent pixels currently owned. A pixel may appear in both sections in which case it is both currently owned and was also previously owned before the last purchase. The ranges are encoded as 32 bit integers start | (end-1)<<16, where start and end are encoded as lx | ly << 8 for local chunk coordinates. Note that ranges use indices in packed local chunk coordinates, they go left-to-right and can span multiple rows (y values). Additionally, the ranges might not be sorted, deduplicated, or optimized. It is up to you to detect overlapping ranges if there are any.


Incoming pixel data

S: v32(24 | 25) u8(cx) u8(cy) [ u32(lx_ly_color) u32(price) ]*

lx_ly_color is formed as lx | ly<<8 | col<<16. lx and ly correspond to local positions within the chunk that this packet is for ((cx, cy)). price is in units of $0.0001, much like chunk data.

For pixels that you now own, code 25 is used, otherwise, code 24 is used.

Placing pixels

Submitting a transaction

C: v32(10) u16(min_x) u16(min_y) u64(max_price) [ u32(x_y_off_col) ]*

Submits a transaction of pixels defined by (min_x + x_off, min_y + y_off) and color col packed into x_y_off_col = x_off | y_off<<8 | col<<16. The pixels must be supplied in ascending position order (i.e sorted by the x_off | y_off<<8 value)

If the price for obtaining ownership of all of these pixels is greater than max_price, the transaction is aborted. A transaction is considered atomic, this means that it either succeeds or fails in its entirety, it is not possible for only some pixels to have been placed and others been rejected.

Note that transactions are bound by the following limits


Transaction confirmation

S: v32(32) [v32(placed_count) u64(final_price)]?

If the main contents of the packet are not present (i.e only the packet ID is present), the transaction has failed. This is usually due to the price being above the max_price value specified, or for another internal server error. Otherwise, placed_count will match the size of your transaction and final_price will hold how much was deducted from your balance for this transaction. To make request-response matching simpler, packet transaction results are sent in the same order as the packet transactions themselves.