Skip to main content
The code on this page is extracted from a sample project that compiles against each SDK release. The guide cannot show code that does not build. Every snippet is regenerated from the release the install step pins, so the code matches the version you install.
This is the executable path through these docs, written for a coding agent with a human on hand for the few steps only a human can do. Follow the steps in order. Each ends with a check that either passes or names the fix. Two reading rules. Treat every account id and project id as an opaque string: read them from tool responses or from your human, never construct or guess one. Where a step says hand-off, stop, tell your human exactly what to do and where, and wait for their confirmation.

Preconditions

  • A Unity 2022.3 LTS or newer project that compiles cleanly.
  • Write access to Packages/manifest.json, Assets/, and the startup scene.
  • A PIO account, and a human who can reach its dashboard.
  • Recommended: the PIO MCP server connected. It provides check_integration, the live readout this guide’s verification loop uses. Without it, each verification falls back to the dashboard through your human.

Step 1: install the SDK

The SDK installs from the public PIO package registry. No key, login or .upmconfig.toml is needed. Add the registry, the SDK and its one dependency to Packages/manifest.json:
Packages/manifest.json
Merge these entries into the existing scopedRegistries and dependencies blocks; every manifest edit in this guide is additive, and replacing the file would drop the project’s built-in modules. The SDK is pre-1.0, so pin the exact version shown, never a range. Installation & versioning carries the upgrade rules. Check: the project compiles and using PIO; resolves. Without the Editor open, compile in batch mode, with UNITY_EDITOR set to the editor binary for the project’s Unity version:
Exit code 0 and no error CS lines in the log mean the install compiles. The log never confirms that the packages resolved. Check Packages/packages-lock.json: it must lock com.pioneeroptimisation.sdk at the pinned version from the PIO registry, and UniTask at the pinned revision. A failed or partial resolve can leave an older cached version compiling, and still exits 0. A registry 401, 403 or 404 in the log means the registry refused the request. Check the registry URL matches the snippet exactly. If it does, hand off to your human. On macOS the editor binary lives at /Applications/Unity/Hub/Editor/<version>/Unity.app/Contents/MacOS/Unity; export it as UNITY_EDITOR once and every later step reuses it.

Step 2: choose a project and collect credentials

Three values connect the SDK to a project: ApiKey, AccountId, and ProjectId. With the MCP server:
  1. Call whoami. It names the account, your scopes, and the account’s projects.
  2. Use the project your human names, or create one with create_project. A project you create starts under a not-child-directed default and accepts events at once. A studio member must confirm the child-directed answer in the project’s settings within 14 days (the confirm_by date in tool responses), or the key is unpublished.
  3. Call get_api_key for the project. The response carries the API key and the account id. It needs an admin credential; a forbidden error names who can help. While the confirmation is outstanding the response carries attestation_pending: true and the confirm_by date; the key works throughout the grace.
Without the MCP server, hand-off: ask your human for all three values from the project’s settings page in the dashboard. Keep the API key out of version control. Put it in an ignored file or an environment variable, and leave serialised credential fields empty in committed scenes and assets.

Step 3: write the integration

Create Assets/Scripts/PioBootstrap.cs containing exactly this file. It initialises once at startup, registers the action definitions with fallbacks, surfaces errors while integrating, requests a decision, and tracks events:
Samples~/BasicIntegration/Assets/Scripts/PioBootstrap.cs
The using static PIO.PIO; line matters. The PIO class shares its name with the PIO namespace, so qualified calls like PIO.Init do not resolve from a script outside that namespace. Each key you register must also exist as an action definition in the project. With the MCP server, call create_action_definition with the name, the candidate actions, and the default. The candidate actions are a game design decision, not something the guide or the tools can infer; take them from your task brief, or ask your human. This sample’s Tutorial definition offers Short, Medium and Long, with Short as the default, matching the code’s fallback. Register the event types the game will track the same way, with register_event_type. Without the MCP server, hand-off: your human creates both in the dashboard. Wire it into the game: add a GameObject named PIO to the first loaded scene and attach PioBootstrap, filling the three credential fields in the Inspector. Headless, or in a fresh project with no scene, create the scene with a throwaway Editor script instead of hand-writing scene YAML. The script only builds the scene; credentials never touch it. PioBootstrap itself reads PIO_API_KEY, PIO_ACCOUNT_ID, and PIO_PROJECT_ID from the environment when its Inspector fields are blank, so no key lands in a committed file:
Assets/Editor/PioSetup.cs
The scene list is appended, never replaced, so an existing project keeps its build entries. Run the script once, then delete the file:
Export the same three variables for every headless run of the game or its tests; the Inspector path needs none of them. This guide configures the SDK in code. The SDK also offers a PioSettings asset (menu PIO/Setup Wizard), which the zero-argument PIO.Init() loads. The PIO/Verify Integration window expects that asset and reports a missing-settings failure even when a code-configured integration works; create the asset, or read past that one check. The whole change, for a game with one decision point:

Step 4: run the game

Enter Play mode and pass through the decision point, so the SDK initialises, serves a decision, and sends events. Headless, drive the same path with a PlayMode test run under -runTests -testPlatform PlayMode. Two pieces of setup come first:
  1. Add com.unity.test-framework (1.1.33) to Packages/manifest.json; step 1’s manifest does not include it.
  2. Test assemblies cannot reference the default Assembly-CSharp, so give Assets/Scripts its own assembly definition. Put the test assembly definition under Assets/Tests/PlayMode. Delete the temporary Assets/Editor/PioSetup.cs after scene setup.
Use the sample’s two assembly definitions, preserving their names and references. The code-block titles identify their source files in the SDK. Copy this file to Assets/Scripts/PIO.Samples.asmdef:
Samples~/BasicIntegration/Assets/Scripts/PIO.Samples.asmdef
Copy this file to Assets/Tests/PlayMode/PIO.Samples.Tests.PlayMode.asmdef:
Samples~/BasicIntegration/Assets/Tests/PlayMode/PIO.Samples.Tests.PlayMode.asmdef
The test, exactly:
Samples~/BasicIntegration/Assets/Tests/PlayMode/PioIntegrationPlayModeTests.cs
It loads the scene from step 3 and lets PioBootstrap.Start() drive the whole loop: init, the decision, and both events, with one flush interval waited so the queue ships. Delete the test assembly after verification if the project does not keep tests. Run it with a results file; for test runs the exit code alone is not a reliable pass signal:
Read the pass or fail from playmode-results.xml.

Step 5: verify

With the MCP server, call check_integration after each run and read three things:
  • status and streaming: never means nothing has arrived yet; integrated with streaming: true means events are flowing now.
  • coverage: track the event types the project expects, listed in coverage.expected. Types under unregistered are arriving without being registered; they still flow through, with a console warning. Register them with register_event_type; without the MCP server, your human does it in the dashboard.
  • next_step: the one thing to do now. Do it, run again, and check again.
Two timing facts matter here. A successful flush writes nothing to the console at any verbosity, and check_integration can lag minutes behind real delivery. After a clean run, poll it patiently rather than reading a single never as failure. Without the MCP server, hand-off: your human opens integration health and reads the same facts, or runs PIO/Verify Integration in the Editor. You are done when check_integration reports status: "integrated" with your event types seen, or your human confirms the dashboard shows events arriving. Integration is one early stage of six; get_project_status names what remains after it, none of which needs code.

When something fails

  • Init fails: PIO.State names the phase, and the OnError line carries the code. Re-read the three credential values before touching code; most failures are a wrong or swapped id.
  • Events sent, console clean, status still never: normal for the first minutes after a fresh integration. Keep polling check_integration; client-side silence is what success looks like.
  • Attestation overdue: a project unconfirmed past its confirm_by date has its key unpublished. Init then fails with a JSON parse error at the auth call, nothing naming attestation. check_integration reports attestation_overdue; confirm in project settings to restore events.
  • Decisions arrive but events never do: decisions and events travel to different services. Leave every *UrlOverride field blank; see Verify your integration. When overrides are set deliberately, for a PIO internal environment, check their values instead of blanking them.
  • INGESTION_FAILED_PERMANENTLY, events dropped after retries: the SDK queues events on disk under Unity’s Company Name and Product Name, not under a PIO project. A machine that ran a different project under the same names retries that stale backlog, then drops it. If check_integration shows this run’s events as seen, the drops were stale and nothing is wrong. To clear the backlog, quit the application, then delete pio_events.db under Application.persistentDataPath. Leave the Product Name alone; changing it moves the whole persistent-data directory, saves included.
  • Any MCP tool error returns error, message, and next_step. Act on next_step rather than retrying the same call.