Partner Integration Guide
Welcome to the Bifrost Partner API. This guide will walk you through everything you need to integrate your platform with Bifrost — from setting up encryption to creating guilds and managing game server billing.
Before you begin
To use the Partner API, you need a Partner Account and an Encryption Key. These are provided by the Bifrost team during onboarding. If you don't have these yet, please contact your Bifrost account manager.
How the Partner API Works
Unlike the standard Bifrost API (which uses OAuth 2.0 tokens), the Partner API uses AES-256-GCM encryption for authentication. Instead of sending a bearer token, you encrypt your entire request payload with your unique encryption key. If the server can decrypt it, that proves you are who you say you are.
Build your payload
Create a JSON object with the data for your request (e.g. guild details, server action).
Include a timestamp and nonce for security.
Encrypt it
Encrypt the JSON string using AES-256-GCM with your partner key.
Base64-encode the result. This becomes the encryptedData field.
Send the GraphQL request
POST the GraphQL mutation to the API with your partnerId and the encrypted payload.
No bearer token needed.
What You'll Need
| Item | Description | Example |
|---|---|---|
partnerId | Your unique partner identifier | your-partner-id |
encryptionKey | Base64-encoded 256-bit AES key | YOUR_BASE64_ENCRYPTION_KEY |
API URL | The Bifrost GraphQL endpoint | https://api.bifrostgaming.com/v1/graphql |
Encryption Guide
Every Partner API request requires you to encrypt your JSON payload using AES-256-GCM. This is an industry-standard authenticated encryption algorithm — it both encrypts your data and verifies it hasn't been tampered with.
Important: Your encryption key is a Base64-encoded string. Before using it, you must Base64-decode it to get the raw 32 bytes. Do not use the Base64 string directly as the key — that won't work.
Encrypted Data Format
After encrypting, you combine three pieces into a single byte array, then Base64-encode the whole thing:
[12-byte IV][ciphertext][16-byte auth tag] → Base64 encode → encryptedData
| Component | Size | What it is |
|---|---|---|
IV | 12 bytes | Initialisation Vector — randomly generated for every request. Never reuse an IV with the same key. |
Ciphertext | Variable | Your JSON payload, encrypted. Same length as the original plaintext. |
Auth Tag | 16 bytes | Authentication tag — proves the data hasn't been modified. GCM generates this automatically. |
Encryption Code Examples
Here's how to encrypt a payload in each language. You can copy this directly into your project:
# Encryption must be done in code before the cURL call.
# Here's a complete Node.js script that encrypts and sends:
node -e "
const crypto = require('crypto');
const https = require('https');
// Your credentials
const PARTNER_ID = 'your-partner-id';
const KEY_BASE64 = 'YOUR_BASE64_ENCRYPTION_KEY';
// Build the payload
const payload = {
action: 'CREATE',
gameServerId: 'your-server-uuid',
mode: 'LIVE',
timestamp: Date.now(),
nonce: crypto.randomBytes(16).toString('hex')
};
// Encrypt
const key = Buffer.from(KEY_BASE64, 'base64');
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
const ciphertext = Buffer.concat([
cipher.update(JSON.stringify(payload), 'utf8'),
cipher.final()
]);
const tag = cipher.getAuthTag();
const encrypted = Buffer.concat([iv, ciphertext, tag]).toString('base64');
console.log('Encrypted payload:', encrypted);
console.log('Length:', encrypted.length, 'characters');
"Required Security Fields
Every encrypted payload — regardless of which endpoint you're calling — must include these two fields to prevent replay attacks:
| Field | Type | Description |
|---|---|---|
timestamp | Number |
Current time as Unix milliseconds (e.g. 1705849200000).
Must be within 5 minutes of the server's clock.
If your request is rejected for an expired timestamp, check that your system clock is accurate.
|
nonce | String | A unique string, at least 16 characters long. Use a UUID or random hex string. Each request must have a different nonce — never reuse one. |
Example security fields
{
"timestamp": 1705849200000,
"nonce": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
...your other fields here...
} Available Partner Endpoints
The Partner API provides two main endpoints. Click on each to see full documentation with request/response schemas and code examples.
partnerCreateGuild
Create a new guild (community) and its owner account in a single request. Includes a 14-day premium trial subscription.
partnerServerAction
Create servers, change billing status, cancel servers, or update owner emails. Handles the full server and account lifecycle. Supports LIVE and TEST modes.
Server Billing Status Lifecycle
Every game server has a billing status. The Partner API lets you transition servers between these statuses. Here's what each status means and which transitions are allowed:
The server is live and will be billed. This is the normal operating state.
Can transition to: ACTIVEFREE, INACTIVE, NOPAYMENT, CANCELLED, CANCELLEDREFUNDED
The server is live but not billed. Used for free-tier, promotional, or internal servers.
Can transition to: ACTIVE, INACTIVE, NOPAYMENT, CANCELLED, CANCELLEDREFUNDED
The server has had no match data for 5+ rolling days. It's still billable but flagged as inactive.
Can transition to: ACTIVE, ACTIVEFREE, NOPAYMENT, CANCELLED, CANCELLEDREFUNDED
The server has payment issues and is not billed.
Can transition to: ACTIVE, ACTIVEFREE, CANCELLED
The server is permanently decommissioned. This is a terminal state — it cannot be changed.
No further transitions allowed.
The server was cancelled within the refund grace period (default: 72 hours from creation). Terminal state.
No further transitions allowed. Only available if the server was created less than 72 hours ago.
LIVE vs TEST mode: When you create or activate a server, you specify
a mode of either
LIVE or
TEST.
TEST servers are completely excluded from billing calculations — use this for development
and testing. LIVE servers are included in billing.
Guild Creation Payload Reference
When calling partnerCreateGuild,
the JSON you encrypt must follow this exact structure:
// This is the JSON you encrypt — NOT the GraphQL query { "user": { "email": "[email protected]", "username": "CommunityOwner", "firstName": "John", "lastName": "Doe", "discordId": "123456789012345678" }, "guild": { "name": "My Awesome Community", "abbreviation": "MAC", "discordUrl": "https://discord.gg/example", "description": "A friendly gaming community", "websiteUrl": "https://example.com", "countries": ["GB", "US"], "is18Plus": true, "isRecruiting": true, "isCompetitive": false, "isPcPlayers": true, "isConsolePlayers": false }, "metadata": { "ownerId": "[email protected]" }, "options": { "sendWelcomeEmail": false }, "timestamp": 1705849200000, "nonce": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6" }
User Fields
| Field | Required | Description |
|---|---|---|
user.email | Required | Valid email address. Must be unique across all Bifrost users. |
user.username | Required | Display name, minimum 2 characters. |
user.firstName | Optional | Owner's first name. |
user.lastName | Optional | Owner's last name. |
user.discordId | Optional | Discord user ID (snowflake). If provided, the Discord account is automatically linked to their Bifrost account. |
Guild Fields
| Field | Required | Description |
|---|---|---|
guild.name | Required | Community name, minimum 2 characters. |
guild.abbreviation | Required | Short code (max 10 chars). Must be globally unique (e.g. "MAC", "504D"). |
guild.discordUrl | Optional | Discord invite URL (e.g. https://discord.gg/example). |
guild.description | Optional | A short description of the community. |
guild.websiteUrl | Optional | Community website URL. |
guild.countries | Required | Array of ISO country codes where the community operates. Must have at least one entry. |
guild.is18Plus | Required | Whether this is an 18+ community. |
guild.isRecruiting | Required | Whether the community is currently accepting new members. |
guild.isCompetitive | Required | Whether this is a competitive/tournament community. |
guild.isPcPlayers | Required | Whether the community includes PC players. |
guild.isConsolePlayers | Required | Whether the community includes console players. |
Options & Metadata
| Field | Required | Description |
|---|---|---|
options.sendWelcomeEmail | Optional |
If true, the owner receives a welcome email with a password reset link (valid 12 hours) and no temporaryPassword is returned.
If false (default), a temporary password is generated and returned in the response for you to share securely.
|
metadata.ownerId | Required |
Must be set to the guild owner's email address (must match user.email).
This is your persistent identifier for targeting this guild in all future server actions
(CREATE, CHANGE_STATUS, DELETE, CHANGE_EMAIL). One ownerId can only be associated with one guild per partner.
The owner's email is managed by you — the user cannot change it themselves in the Bifrost control panel.
|
metadata.* | Optional | You may include additional fields in metadata for your own tracking purposes (e.g. internal customer ID). These are stored alongside the guild record but not used by Bifrost. |
Server Action Payload Reference
When calling partnerServerAction,
the JSON you encrypt depends on the action you want to perform:
CREATE — Register a new game server
This is the main action for onboarding servers. It creates a fully configured game server on the Bifrost platform,
links it to your partner guild, and activates billing. You must have already created a guild
via partnerCreateGuild before creating servers.
{
"action": "CREATE",
"ownerId": "[email protected]",
"serverGameType": "HLL",
"serverName": "My Community Server #1",
"serverIP": "203.0.113.50",
"serverQueryPort": 27015,
"serverRCONPort": 27020,
"serverRCONPassword": "MySecureRconPass123!",
"serverCountry": "US",
"serverTimezone": "America/New_York",
"serverPlatform": "PC",
"mode": "LIVE",
"timestamp": 1705849200000,
"nonce": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
} Important: The response includes a serverId. Store this value — you will need it for all future
CHANGE_STATUS and DELETE actions on this server.
CHANGE_STATUS — Change a server's billing state
{
"action": "CHANGE_STATUS",
"ownerId": "[email protected]",
"gameServerId": "server-uuid-from-create-response",
"status": "NOPAYMENT",
"reason": "Payment method declined",
"timestamp": 1705849200000,
"nonce": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
} DELETE — Cancel a server permanently
Permanently cancels a server. The server record is not deleted from the database — it is set to
CANCELLED status and deactivated. This is a terminal state
and cannot be reversed.
{
"action": "DELETE",
"ownerId": "[email protected]",
"gameServerId": "server-uuid-from-create-response",
"reason": "Customer cancelled subscription",
"timestamp": 1705849200000,
"nonce": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
} CHANGE_EMAIL — Update a guild owner's email
Updates the guild owner's email address across the identity provider (Keycloak), the database, and the guild metadata — all atomically. If either the identity provider or database update fails, everything is rolled back. The user's login email changes immediately. Use this when a customer changes their email address.
{
"action": "CHANGE_EMAIL",
"ownerId": "[email protected]",
"newEmail": "[email protected]",
"timestamp": 1705849200000,
"nonce": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
} Important: After a successful CHANGE_EMAIL, use the new email as the
ownerId in all future requests. The old email will no longer work.
CREATE Action Fields
| Field | Required | Description |
|---|---|---|
action | Required | Must be "CREATE" |
ownerId | Required | The guild owner's email address (as provided in metadata.ownerId during guild creation). Identifies which guild to link the server to. |
serverGameType | Required | The game type. Currently supported: "HLL" (Hell Let Loose) |
serverName | Required | Display name for the server (e.g. "My Community Server #1"). A unique server key is auto-generated from this. |
serverIP | Required | The server's IP address (e.g. "203.0.113.50"). Must be reachable by the Bifrost platform. |
serverQueryPort | Required | The server's Steam Query port (integer, e.g. 27015). Used for server status polling. |
serverRCONPort | Required | The server's RCON port (integer, e.g. 27020). Used for remote console commands. |
serverRCONPassword | Required | The server's RCON password. Stored encrypted at rest. |
serverCountry | Optional | ISO country code where the server is hosted (e.g. "US", "DE", "GB"). Defaults to "Unknown". |
serverTimezone | Optional | IANA timezone (e.g. "America/New_York", "Europe/London"). Defaults to "UTC". |
serverPlatform | Optional | "PC" (default) or "Console". |
mode | Optional | "LIVE" (default, billable) or "TEST" (excluded from billing). Use TEST for development/staging servers. |
CHANGE_STATUS & DELETE Action Fields
| Field | Required | Description |
|---|---|---|
action | Required | "CHANGE_STATUS" or "DELETE" |
ownerId | Required | The guild owner's email address. The server must belong to a guild with this ownerId. Prevents cross-guild operations. |
gameServerId | Required | The UUID returned by a previous CREATE action. You can also use serverId as an alias. |
status | CHANGE_STATUS only | The new billing status. Must be a valid transition from the current status (see lifecycle diagram above). |
reason | Optional | Human-readable explanation for the action. Stored in the audit log for accountability. |
CHANGE_EMAIL Action Fields
| Field | Required | Description |
|---|---|---|
action | Required | Must be "CHANGE_EMAIL" |
ownerId | Required | The guild owner's current email address. |
newEmail | Required | The new email address. Must not already be in use by another user. After success, use this as the ownerId in all future requests. |
All actions must also include timestamp (Unix milliseconds, within 5 minutes of server time)
and nonce (unique string, at least 16 characters). See the Security Fields section above.
Error Codes Reference
Both partner endpoints return a statusCode field
that follows HTTP conventions. Here's what each code means and what to do about it:
| Code | Meaning | What to do |
|---|---|---|
200 | Success | The action completed. Check the response for details. |
201 | Created | A new resource was created (guild or game server). Store the returned IDs. |
206 | Missing fields | The partnerId or encryptedData was missing from the request. |
400 | Bad request | Decryption failed (wrong key?), or the payload failed validation. Check the message for details. |
401 | Unauthorized | Partner not found, inactive, or no encryption key configured. Check your partnerId. |
403 | Conflict / Forbidden | The guild abbreviation, email, ownerId, or Discord ID is already in use; or the server does not belong to your partner/ownerId. |
404 | Not found | The specified server ID doesn't exist. Double-check the UUID. |
409 | State conflict | The status transition is not allowed (e.g. trying to change a CANCELLED server), or the refund grace period has expired. |
500 | Server error | Something went wrong on our end. Retry after a short delay. If it persists, contact support. |
Troubleshooting
"Decryption failed" (400)
This means the server couldn't decrypt your payload. Common causes:
- You're using the Base64 string directly as the key instead of Base64-decoding it first
- Your key has been regenerated — ask your account manager for the latest key
- The byte order is wrong — it must be
[IV][ciphertext][tag], not[IV][tag][ciphertext] - You're using a 128-bit or 192-bit AES variant instead of 256-bit
"Timestamp expired" or "Timestamp too far in the future" (400)
Your timestamp is more than 5 minutes from the server's clock. Check:
- Your system clock is accurate (use NTP sync)
- You're sending Unix milliseconds, not seconds (multiply by 1000)
- You're generating the timestamp immediately before sending, not caching it
"Nonce must be at least 16 characters" (400)
Your nonce is too short. Use uuid.uuid4().hex (Python),
Guid.NewGuid().ToString("N") (.NET), or
crypto.randomBytes(16).toString('hex') (Node.js)
to generate a 32-character hex string.
"Partner not found" or "Partner inactive" (401)
Double-check your partnerId — this is your unique identifier
(e.g. "your-partner-id"), not a UUID. If you're sure it's correct,
your partner account may have been deactivated. Contact your Bifrost account manager.
"Invalid state transition" (409)
You're trying to change a server to a status that isn't allowed from its current state. For example, you can't change a CANCELLED server to anything — it's a terminal state. See the billing lifecycle diagram above for valid transitions.
Need Help?
If you're stuck or something isn't working as expected, reach out to your Bifrost account manager. Please include:
- Your
partnerId - The full error response (including
statusCodeandmessage) - The approximate time of the request (so we can check server logs)
- The language/platform you're using