HTTP API reference
The Luau SDK wraps the game API, so you only need this page to call Rolink from another language, to understand what the linking page does, or to debug.
Conventions
Section titled “Conventions”-
Base URL. Every path on this page is relative to
https://api.rolink.tech. The SDK uses it by default. -
HTTPS and JSON. Request bodies are JSON. Every response is JSON, except the linking page (HTML) and the redirects of the sign-in flow.
-
Amounts are decimal strings in token units, such as
"1.5". Raw base units are also strings, such as"1500000000". -
Times such as
linkedAtare Unix timestamps in milliseconds. -
Errors share one shape, with a stable
codeand a human-readablemessage:{ "error": { "code": "wallet_not_linked", "message": "Player 42 has not linked a wallet" } }Every code is listed in Error codes.
These errors can come from any route:
| Code | HTTP | When |
|---|---|---|
invalid_json | 400 | A route that reads a body got one that is not valid JSON, or no body at all. |
invalid_request | 400 | The body does not match the route’s schema. The message lists each problem as path: problem, separated by ; . |
not_found | 404 | No such route. |
internal | 500 | An unexpected error, such as an RPC failure during a read. |
An invalid_request example:
{ "error": { "code": "invalid_request", "message": "amount: Invalid input: expected string, received number; idempotencyKey: Too small: expected string to have >=1 characters" }}Authentication
Section titled “Authentication”Routes under /v1 are for your game servers. Each request must carry a project API key in the x-api-key header:
x-api-key: rlk_live_<43 characters>Create keys in the dashboard, under your project’s API keys. A key belongs to one project, and every /v1 route reads and writes only that project’s data: its links, its transactions, its managed wallet and its cluster. Keys are checked before anything else on every /v1 route.
| Code | HTTP | When |
|---|---|---|
unauthorized | 401 | The x-api-key header is missing, malformed, revoked, or belongs to a deleted project. |
A revoked key stops working within about 30 seconds.
Quotas
Section titled “Quotas”Every /v1 request with a valid key counts toward the project’s beta quotas, whatever its answer. Responses carry no rate-limit headers; the dashboard’s Overview shows the project’s usage.
| Code | HTTP | When |
|---|---|---|
rate_limited | 429 | More than 600 requests in the current minute for this project. |
daily_quota_exceeded | 429 | The project used its 100,000 requests for the current UTC day. |
Transactions have their own daily quota, daily_tx_quota_exceeded. All quotas are listed in Limits and quotas.
Routes
Section titled “Routes”| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /health | none | Service status |
GET | /v1/project | API key | The key’s project |
GET | /v1/players/:userId/wallet | API key | One player’s wallet |
POST | /v1/players/wallets | API key | Many players’ wallets |
GET | /v1/wallets/:address/balance | API key | SOL balance |
GET | /v1/wallets/:address/tokens | API key | SPL and Token-2022 balances |
GET | /v1/wallets/:address/assets | API key | NFTs the wallet owns |
GET | /v1/signatures/:signature | API key | Transaction signature status |
POST | /v1/tx/transfer-sol | API key | Send SOL |
POST | /v1/tx/transfer-token | API key | Send a token |
POST | /v1/tx/mint-token | API key | Mint a token |
GET | /v1/tx/:idempotencyKey | API key | Transaction request status |
GET | /link/:slug | none | A project’s linking page |
GET | /api/link/:slug/me | session cookie | Current player and link |
POST | /api/link/:slug/challenge | same origin + session | Get a message to sign |
POST | /api/link/:slug/verify | same origin + session | Prove wallet ownership and link |
POST | /api/link/:slug/unlink | same origin + session | Remove the link |
GET | /auth/roblox/start | none | Start a player’s Sign in with Roblox |
GET | /auth/roblox/callback | sign-in cookie | Finish a player’s Sign in with Roblox |
POST | /auth/wallet/challenge | same origin | Get the dashboard sign-in message |
POST | /auth/wallet/verify | same origin + sign-in cookie | Sign a developer in to the dashboard |
POST | /auth/logout | same origin | End the session |
The dashboard, at https://api.rolink.tech/dashboard, is a web app for signed-in developers. Its pages are not part of the API.
Health
Section titled “Health”GET /healthNo authentication, and not counted toward any quota. Reports that the service is up.
{ "ok": true, "wallets": "turnkey" }| Field | Meaning |
|---|---|
ok | true when the API answers. |
wallets | The provider that holds managed wallets: turnkey, or none while managed wallets are off. With none, creating a project wallet answers wallets_unavailable and transactions are unavailable. Everything else works. |
Game API
Section titled “Game API”Project
Section titled “Project”GET /v1/projectx-api-key: <key>Returns the project the key belongs to.
{"id": "prj_3fk9Qx2LmP0aB7cD","name": "My Game","slug": "my-game","cluster": "devnet","walletAddress": "8Kip13Ac8mYrW6Qq62ERSEFbpu5PVSA9U4xScEk1hpRT","linkUrl": "https://api.rolink.tech/link/my-game","events": true}| Field | Meaning |
|---|---|
id | The project’s ID. |
name | The name set in the dashboard. |
slug | Set from the name when the project is created. It doesn’t change when you rename the project. |
cluster | devnet or mainnet, chosen when the project is created. Every read and transaction uses it. |
walletAddress | The managed wallet’s address, or null until it is created in the dashboard. |
linkUrl | The project’s hosted linking page. |
events | true when the project has both a universe ID and an Open Cloud API key, so live events are published. |
Player wallet
Section titled “Player wallet”GET /v1/players/42/walletx-api-key: <key>Returns the wallet a player linked to this project. address and linkedAt are null when there is none. Answered from Rolink’s database, not the chain.
{ "userId": 42, "address": "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin", "linkedAt": 1791429403583 }| Code | HTTP | When |
|---|---|---|
invalid_user_id | 400 | :userId is not a positive integer. |
Bulk wallet lookup
Section titled “Bulk wallet lookup”POST /v1/players/walletsx-api-key: <key>Content-Type: application/json
{ "userIds": [42, 43] }userIds is an array of up to 200 positive integers. The response has one key per requested id, as a string, set to the address or null. The whole lookup counts as one request.
{ "wallets": { "42": "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin", "43": null } }| Code | HTTP | When |
|---|---|---|
invalid_json | 400 | Missing or malformed body. |
invalid_request | 400 | userIds is missing, has more than 200 entries, or contains something other than a positive integer. |
SOL balance
Section titled “SOL balance”GET /v1/wallets/9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin/balancex-api-key: <key>Read on the project’s cluster at confirmed commitment, and cached for about 10 seconds.
{ "address": "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin", "lamports": "1500000000", "sol": "1.5" }| Code | HTTP | When |
|---|---|---|
invalid_address | 400 | :address is not a Solana address. |
Token balances
Section titled “Token balances”GET /v1/wallets/9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin/tokens?mint=4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDUx-api-key: <key>Returns one entry per mint, with the amounts of all the wallet’s token accounts for that mint added together. Token and Token-2022 accounts are both included. mint is optional:
- without it, every mint the wallet has a token account for is listed
- with it, the list always has exactly one entry, with
"0"amounts if the wallet holds none
Read at confirmed commitment and cached for about 10 seconds.
{ "address": "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin", "tokens": [ { "mint": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU", "amount": "2500000", "decimals": 6, "uiAmount": "2.5" } ]}| Code | HTTP | When |
|---|---|---|
invalid_address | 400 | :address or mint is not a Solana address. |
invalid_mint | 400 | mint does not exist on the project’s cluster or is not a token mint. |
Assets
Section titled “Assets”GET /v1/wallets/9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin/assets?collection=<collection address>x-api-key: <key>Lists the NFTs the wallet owns. With collection, only those in that collection.
- With a DAS endpoint for the project’s cluster, Rolink reads them through the DAS API:
getAssetsByOwner, orsearchAssetsgrouped by the collection. DAS also sees compressed NFTs and Metaplex Core assets. - Without one, Rolink reads classic NFTs straight from the chain: Metaplex Token Metadata NFTs, programmable NFTs and editions whose supply is 1.
imageis thennull, because it lives in the off-chain metadata.
Either way, collection is set only for a verified collection, so a mint can’t claim to belong to yours. Only the first 1,000 assets are read. Cached for about 10 seconds.
{ "address": "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin", "assets": [ { "id": "<asset address>", "name": "Founder #12", "collection": "<collection address>", "image": "https://example.com/founder-12.png", "interface": "V1_NFT" } ]}name, collection, image and interface are null when the asset’s metadata does not have them.
| Code | HTTP | When |
|---|---|---|
invalid_address | 400 | :address or collection is not a Solana address. |
Signature status
Section titled “Signature status”GET /v1/signatures/5z99PpyTfnCYHDAbyVoMFuHD3gWQFis2izi9NSJKEbijXWUQQnuWxcnAKUFxAhSJcJMExR9MfN69R1PhsvGiQYeux-api-key: <key>Looks up any transaction signature on the project’s cluster, searching the RPC node’s transaction history. Not cached.
{ "signature": "5z99PpyTfnCYHDAbyVoMFuHD3gWQFis2izi9NSJKEbijXWUQQnuWxcnAKUFxAhSJcJMExR9MfN69R1PhsvGiQYeu", "status": "finalized", "slot": 312345678, "error": null}| Field | Values |
|---|---|
status | processed, confirmed, finalized, failed, or unknown when the node has no record of the signature |
slot | The slot it landed in, or null when unknown |
error | The on-chain error as a JSON string when failed, such as "{\"InstructionError\":[1,{\"Custom\":1}]}", otherwise null |
| Code | HTTP | When |
|---|---|---|
invalid_signature | 400 | :signature is not a base58 transaction signature. |
Transactions
Section titled “Transactions”The three transaction routes sign and send with the project’s managed wallet. The wallet must exist, and each request must fit the transaction policy set in the dashboard. The guide is Send transactions.
Request body
Section titled “Request body”| Field | Type | Rules |
|---|---|---|
to | object | Exactly one of { "userId": 42 } (a positive integer, resolved to the wallet the player linked to this project) or { "address": "<base58>" }. No other keys. |
mint | string | Token routes only. A mint listed under Allowed mints in the policy. |
amount | string | A decimal in token units matching ^\d+(\.\d+)?$, such as "1.5". Greater than zero, with no more decimals than the asset has (9 for SOL). |
idempotencyKey | string | 1 to 128 characters. Unique per intended payment within the project. |
TxResult
Section titled “TxResult”Every transaction route, and the status route, answers 200 with a TxResult:
{ "idempotencyKey": "quest:42:intro", "kind": "transfer-token", "status": "confirmed", "signature": "5z99PpyTfnCYHDAbyVoMFuHD3gWQFis2izi9NSJKEbijXWUQQnuWxcnAKUFxAhSJcJMExR9MfN69R1PhsvGiQYeu", "recipient": "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin", "error": null}| Field | Meaning |
|---|---|
kind | transfer-sol, transfer-token or mint-token |
status | confirmed: it landed. failed: nothing reached the recipient, see error. submitted: sent, not yet confirmed. pending: another request with this key is being prepared. |
signature | Set once the managed wallet signed the transaction, otherwise null. |
recipient | The wallet address the request resolved to. |
error | Why it failed, otherwise null. |
The request waits up to 15 seconds for confirmation. If it takes longer, the answer is submitted and Rolink keeps checking the signature every 1.5 seconds in the background, with a sweep every 30 seconds for anything still submitted. The final result arrives as a tx event and through GET /v1/tx/:idempotencyKey.
A failed status is an answer, not an error. For example, when the network rejects the transaction in simulation (the managed wallet is short of funds, or is not the mint authority), the route answers 200 with "status": "failed" and the simulation error in error. The hosted API reports it in a coded form: Solana error #-32002; Decode this error by running followed by an npx @solana/errors decode command that turns it back into the readable message, Transaction simulation failed.
{ "idempotencyKey": "tournament:7:42", "kind": "transfer-sol", "status": "failed", "signature": "4huotLhmwu1MzxAgG1z8fhctptGdAsG3teAo1QPcxQkddj3jiGmxuK4WQydFK9j8gYUCDWNaSkMQePLdAGBbpEcf", "recipient": "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin", "error": "Solana error #-32002; Decode this error by running `npx @solana/errors decode -- -32002 '<details>'`"}A transaction is reported failed with Transaction expired before it was confirmed only when that is proven: the finalized chain is more than 32 blocks past the blockhash’s last valid height, and a node that has seen that finalized slot still has no record of the signature.
Idempotency
Section titled “Idempotency”Rolink stores every key a project uses, permanently, so these rules always hold:
- Same key, same body, earlier attempt
pending,submittedorconfirmed: Rolink returns that attempt and sends nothing new. - Same key, same body, earlier attempt
failed: Rolink sends again, because a failed attempt moved no funds. - Same key, different body:
409 idempotency_conflict. The body is compared as sent, so{ "userId": 42 }and{ "address": "<the same wallet>" }differ, and so do"1"and"1.0".
A request refused before step 14 of the order of checks never reserves its key. Fix the request and send it again with the same key. A 502 at step 14 is different: the attempt is recorded as failed with that body, so retry it with the same body and key.
Order of checks
Section titled “Order of checks”A transaction request is checked in this order. The first failure is returned.
| Step | Code | HTTP |
|---|---|---|
| 1. API key | unauthorized | 401 |
| 2. Request quota | rate_limited, daily_quota_exceeded | 429 |
| 3. Body is JSON | invalid_json | 400 |
| 4. Body matches the schema | invalid_request | 400 |
5. mint is an address (token routes) | invalid_address | 400 |
| 6. The managed wallet exists | wallet_not_configured | 409 |
| 7. The key is new, or was used for the same body | idempotency_conflict | 409 |
8. to resolves to a wallet | wallet_not_linked (404), invalid_address (400) | |
| 9. The asset is allowed | sol_transfers_disabled (SOL), mint_not_allowed (tokens) | 403 |
| 10. The mint exists on the cluster (token routes) | invalid_mint | 400 |
| 11. The amount is valid and within the per-transaction limit | invalid_amount (400), amount_over_limit (403), invalid_limit_config (422) | |
| 12. The daily transaction quota | daily_tx_quota_exceeded | 429 |
| 13. Recipient, player and budget limits | recipient_rate_limited, player_rate_limited, hourly_budget_exceeded (429), invalid_limit_config (422) for a budget the asset’s decimals cannot represent | |
| 14. The transaction can be built and signed | rpc_error, wallet_unavailable | 502 |
When step 7 finds an earlier attempt that is not failed, Rolink returns it right there. Steps 8 to 13 have no side effects, and the key is reserved only after step 13, in the same step as the limit checks.
Transfer SOL
Section titled “Transfer SOL”POST /v1/tx/transfer-solx-api-key: <key>Content-Type: application/json
{ "to": { "userId": 42 }, "amount": "0.05", "idempotencyKey": "tournament:7:42" }Sends SOL from the managed wallet with a System Program transfer. Disabled until the policy has a Max SOL per transfer. Answers a TxResult with "kind": "transfer-sol".
Transfer a token
Section titled “Transfer a token”POST /v1/tx/transfer-tokenx-api-key: <key>Content-Type: application/json
{ "to": { "address": "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin" }, "mint": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU", "amount": "25", "idempotencyKey": "quest:42:intro"}Sends a token from the managed wallet’s associated token account with a checked transfer, under the mint’s own program (Token or Token-2022). The recipient’s associated token account is created first if needed, paid for by the managed wallet. Answers a TxResult with "kind": "transfer-token".
Mint a token
Section titled “Mint a token”POST /v1/tx/mint-tokenx-api-key: <key>Content-Type: application/json
{ "to": { "userId": 42 }, "mint": "<mint address>", "amount": "1", "idempotencyKey": "badge:42:founder" }Mints new tokens to the recipient’s associated token account, creating it if needed. The managed wallet must be the mint authority. If it is not, the network rejects the transaction and the answer is "status": "failed". Answers a TxResult with "kind": "mint-token".
Transaction status
Section titled “Transaction status”GET /v1/tx/quest%3A42%3Aintrox-api-key: <key>Returns the stored TxResult for a key. URL-encode the key. It answers right away. If the request is still submitted, Rolink resumes checking its confirmation in the background. It works whether or not the managed wallet exists.
| Code | HTTP | When |
|---|---|---|
not_found | 404 | The project has no request with this key. |
Linking routes
Section titled “Linking routes”Each project has a hosted linking page at /link/<slug>, where players sign in with Roblox through the project’s own OAuth app and prove they own a wallet. These routes serve it. They’re documented so you can understand and debug the flow. Game servers never call them.
- Per project. The
:slugin each path is the project’s slug. Every link created here belongs to that project only. - Session. Signing in sets a player session cookie,
__Host-rolink_session, for one hour. It isHttpOnly,Secure,SameSite=LaxandPath=/, and signed with HMAC-SHA256. It belongs to the project whose linking page the player signed in on. On another project’s routes it counts as no session, and a new sign-in replaces it. - Same origin. Every
POSTroute requires anOriginheader equal to the API’s origin, checked first. A cross-site page cannot forge these requests.
| Code | HTTP | When |
|---|---|---|
bad_origin | 403 | A POST without a matching Origin header. |
not_signed_in | 401 | A route that needs a session got no valid, unexpired session cookie for this project. |
project_not_found | 404 | No active project has this slug. |
Linking page
Section titled “Linking page”GET /link/my-gameServes the project’s linking page as HTML. For an unknown slug, or a deleted project, it answers 404 with a short HTML page: “No game uses this link.” The page is sent with these headers, so it cannot be framed:
Content-Security-Policy: frame-ancestors 'none'; base-uri 'none'; form-action 'self'X-Frame-Options: DENYReferrer-Policy: same-originCache-Control: no-storeUntil the project has a Roblox OAuth app saved (a client ID and a client secret), the page disables Continue with Roblox and shows “This game hasn’t set up Roblox sign-in yet.”
When sign-in fails, the callback redirects back to this page with ?error=<code>, and the page shows a matching message. See Sign-in redirect errors.
Current session
Section titled “Current session”GET /api/link/my-game/meReturns the signed-in player and their link in this project. Without a valid session for this project, both are null.
{ "user": { "id": 42, "name": "builderman", "displayName": "Builderman", "avatar": "https://tr.rbxcdn.com/..." }, "link": { "address": "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin", "linkedAt": 1791429403583 }}{ "user": null, "link": null }name is the Roblox username, displayName the display name and avatar the profile picture URL from Roblox, or null. link is null when the player has not linked a wallet to this project.
Request a challenge
Section titled “Request a challenge”POST /api/link/my-game/challengeOrigin: <API origin>Cookie: __Host-rolink_session=<session>Content-Type: application/json
{ "address": "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin" }Returns a single-use nonce and the message the wallet must sign. The challenge expires after 10 minutes.
{"nonce": "f9f0a3be5b153047d5aaec41d21e7b5d","message": "api.rolink.tech wants you to sign in with your Solana account:\n9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin\n\nLink this wallet to Roblox account @builderman (42) in My Game. Signing is free and does not send a transaction.\n\nURI: https://api.rolink.tech/link/my-game\nVersion: 1\nChain ID: devnet\nNonce: f9f0a3be5b153047d5aaec41d21e7b5d\nIssued At: 2026-10-08T03:16:43.594Z\nExpiration Time: 2026-10-08T03:26:43.594Z"}The message follows the Sign-In With Solana layout, so wallets that support it can warn when the domain does not match the site asking for the signature:
api.rolink.tech wants you to sign in with your Solana account:9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin
Link this wallet to Roblox account @builderman (42) in My Game. Signing is free and does not send a transaction.
URI: https://api.rolink.tech/link/my-gameVersion: 1Chain ID: devnetNonce: f9f0a3be5b153047d5aaec41d21e7b5dIssued At: 2026-10-08T03:16:43.594ZExpiration Time: 2026-10-08T03:26:43.594ZThe first line is the API’s host, My Game is the project’s name, URI is the project’s linking page and Chain ID is the project’s cluster.
| Code | HTTP | When |
|---|---|---|
bad_origin | 403 | Missing or foreign Origin. |
project_not_found | 404 | Unknown slug. |
not_signed_in | 401 | No valid session. |
invalid_json, invalid_request | 400 | The body is not { "address": string }. |
invalid_address | 400 | address is not a Solana address. |
Verify a signature
Section titled “Verify a signature”POST /api/link/my-game/verifyOrigin: <API origin>Cookie: __Host-rolink_session=<session>Content-Type: application/json
{ "nonce": "f9f0a3be5b153047d5aaec41d21e7b5d", "signature": "<base64>" }signature is the base64-encoded 64-byte ed25519 signature of message, encoded as UTF-8. Rolink consumes the nonce, checks the signature against the challenge’s address, and links the wallet to the signed-in player in this project.
{ "link": { "address": "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin", "linkedAt": 1791429403583 } }Within a project, a wallet belongs to one Roblox account at a time, and a player has one wallet at a time. Linking replaces the player’s previous wallet and takes the wallet away from any other account that had it in this project. Afterwards Rolink publishes:
wallet_unlinkedfor the other account, if the wallet was linked to onewallet_linkedfor this player
| Code | HTTP | When |
|---|---|---|
bad_origin | 403 | Missing or foreign Origin. |
project_not_found | 404 | Unknown slug. |
not_signed_in | 401 | No valid session. |
invalid_json, invalid_request | 400 | The body is not { "nonce": string, "signature": string }. |
challenge_expired | 400 | The nonce is unknown, already used, older than 10 minutes, issued for another project, or issued to another player. Request a new challenge. |
invalid_signature | 400 | The signature does not decode to 64 bytes (Malformed signature), or does not match the wallet (The signature does not match this wallet). |
Unlink
Section titled “Unlink”POST /api/link/my-game/unlinkOrigin: <API origin>Cookie: __Host-rolink_session=<session>Removes the signed-in player’s link in this project and publishes wallet_unlinked if there was one. Answers the same way when there was nothing to remove.
{ "ok": true }| Code | HTTP | When |
|---|---|---|
bad_origin | 403 | Missing or foreign Origin. |
project_not_found | 404 | Unknown slug. |
not_signed_in | 401 | No valid session. |
Sign-in routes
Section titled “Sign-in routes”There are two sign-ins, and Rolink has no Roblox app of its own:
- Players sign in with Roblox on a project’s linking page, through that project’s own Roblox OAuth app. Its client ID and secret are saved in the project’s settings.
- Developers sign in to the dashboard by signing a message with a Solana wallet.
Start Roblox sign-in
Section titled “Start Roblox sign-in”GET /auth/roblox/start?next=/link/my-gameStarts Sign in with Roblox (OAuth 2.0 with PKCE) for the linking page in next, through that project’s Roblox OAuth app. The route stores a random state, the PKCE verifier, next and the project in a signed cookie valid for 10 minutes, then redirects (302) to Roblox’s authorization page with the project’s client ID, the redirect URI https://api.rolink.tech/auth/roblox/callback, the scopes openid profile and an S256 code challenge.
Instead of an error body, it redirects in these cases, without going to Roblox:
| Case | Redirects to |
|---|---|
next is missing, or not a linking page or dashboard path on this site | /dashboard |
next is a dashboard path: the dashboard uses wallet sign-in | next |
No active project has the slug in next | next, which answers “No game uses this link.” |
| The project has no Roblox OAuth app saved | next?error=oauth_not_configured |
Roblox sign-in callback
Section titled “Roblox sign-in callback”GET /auth/roblox/callback?code=<code>&state=<state>Roblox redirects here. It’s the redirect URL every project registers on its Roblox OAuth app. Rolink deletes the sign-in cookie, checks state, exchanges code for a token with the project’s client ID and secret and the PKCE verifier, reads the user’s identity, then revokes the Roblox token, since it only needed the identity. It starts a player session for that project (one hour) and redirects to the linking page.
On failure it redirects to the linking page with ?error=<code> instead of answering an error body. Without a valid sign-in cookie it can’t tell which page that is, and redirects to /dashboard?error=oauth_state_mismatch. The codes are listed in Sign-in redirect errors.
Start a dashboard sign-in
Section titled “Start a dashboard sign-in”POST /auth/wallet/challengeOrigin: <API origin>Content-Type: application/json
{ "address": "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin", "next": "/dashboard" }address is the wallet signing in. next, optional, is the dashboard page to land on afterwards; anything that isn’t a dashboard path becomes /dashboard. Rolink answers with the message the wallet must sign, and keeps the address, the message and next in a signed HttpOnly cookie, __Host-rolink_signin, valid for 10 minutes. Nothing is written to the database.
{"message": "api.rolink.tech wants you to sign in with your Solana account:\n9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin\n\nSign in to the Rolink dashboard. Signing is free and does not send a transaction.\n\nURI: https://api.rolink.tech/dashboard\nVersion: 1\nChain ID: mainnet\nNonce: 0b6c2f4e9a1d3e5f7a8b9c0d1e2f3a4b\nIssued At: 2026-10-08T03:16:43.594Z\nExpiration Time: 2026-10-08T03:26:43.594Z"}The message follows the Sign-In With Solana layout:
api.rolink.tech wants you to sign in with your Solana account:9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin
Sign in to the Rolink dashboard. Signing is free and does not send a transaction.
URI: https://api.rolink.tech/dashboardVersion: 1Chain ID: mainnetNonce: 0b6c2f4e9a1d3e5f7a8b9c0d1e2f3a4bIssued At: 2026-10-08T03:16:43.594ZExpiration Time: 2026-10-08T03:26:43.594Z| Code | HTTP | When |
|---|---|---|
bad_origin | 403 | Missing or foreign Origin. |
invalid_json, invalid_request | 400 | The body is not { "address": string, "next"?: string }. |
invalid_address | 400 | address is not a Solana address. |
Finish a dashboard sign-in
Section titled “Finish a dashboard sign-in”POST /auth/wallet/verifyOrigin: <API origin>Cookie: __Host-rolink_signin=<pending sign-in>Content-Type: application/json
{ "signature": "<base64>" }signature is the base64-encoded 64-byte ed25519 signature of message, encoded as UTF-8. Rolink deletes the sign-in cookie, verifies the signature against the address it was issued for, and signs the developer in: the wallet address is the account, created on its first sign-in. It sets the developer session cookie, __Host-rolink_dev, for 12 hours, and answers with the page to open.
{ "next": "/dashboard" }| Code | HTTP | When |
|---|---|---|
bad_origin | 403 | Missing or foreign Origin. |
challenge_expired | 400 | No valid sign-in cookie: it is missing, older than 10 minutes, or was already used. Request a new challenge. |
invalid_json, invalid_request | 400 | The body is not { "signature": string }. |
invalid_signature | 400 | The signature does not decode to 64 bytes (Malformed signature), or does not match the wallet (The signature does not match this wallet). |
Log out
Section titled “Log out”POST /auth/logoutOrigin: <API origin>Clears the player and developer sessions. Needs the same-origin header but not a session. Answers { "ok": true }, or redirects to / when the request accepts HTML.
{ "ok": true }| Code | HTTP | When |
|---|---|---|
bad_origin | 403 | Missing or foreign Origin. |
