The Playable Ad Export Matrix: Six Delivery Contracts, One Creative Core

A route-by-route architecture for turning one playable source system into six defensible delivery artifacts without blending package, runtime, MRAID, and validation rules.

By
Hookin Team, Playable Systems & Platform Engineering
Published
July 29, 2026
Reading time
13 min read
Reads
133 reads
On this page
  1. The same playable can be six different deliverables
  2. Start with a route ID, not a logo
  3. The cross-route matrix
  4. Read the Google row with both sources open
  5. Keep Unity's three surfaces in three boxes
  6. Share source state, then adapt the edges
  7. Turn the matrix into a build contract
  8. A profile is only as current as its evidence
  9. Make the export decision before polish is finished

A producer sends final-playable.zip to six destinations: Google Ads, AppLovin, Unity Ads, Unity Exchange, ironSource Exchange, and Meta. The mechanic is finished. The art is approved. The filename says “final.” Yet the handoff has already failed, because those six destinations do not agree on what the deliverable is.

One route expects a ZIP. Another expects exactly one HTML file. One exchange accepts a snippet that can download HTTPS assets. One uploader generally prohibits those requests. Two routes under the same corporate family require different MRAID versions and opposite startup behavior. For Meta, several playable-specific technical fields cannot be responsibly filled from publicly accessible documentation at all.

The answer is not six separately maintained games. It is one creative source system that produces six route-specific artifacts. “One creative core” means shared state, mechanics, source art, and layout intent. It does not mean one runtime wrapper, one file, one byte limit, or one CTA bridge.

The same playable can be six different deliverables

Calling an export “HTML5” describes a technology family, not a delivery contract. Even “MRAID compliant” is incomplete: AppLovin documents MRAID 2.0, Unity Ads user acquisition documents MRAID 3.0, and ironSource Exchange documents MRAID 2.0. The version number still does not answer package shape, remote-origin policy, readiness, audio, first-touch behavior, or official preview path.

A useful export name therefore identifies the delivery route, not just the company: google_ads_app_campaign_zip, applovin_single_html_mraid2, unity_ads_ua_single_html_mraid3, unity_exchange_mraid3, ironsource_exchange_mraid2_tag, or meta_playable_ads_account_verified. A profile called unity has already hidden a decision the build must eventually make.

If you are still deciding how the source experience should be rendered, the Canvas, Phaser, and Three.js comparison is the upstream question. The export matrix begins after that choice: it governs how the finished source becomes a host-specific delivery artifact.

Before minification or inlining, write down the actual transaction. “Unity campaign” is not enough. Is this a Unity Ads user-acquisition upload or a Unity Exchange programmatic integration? “ironSource” is not enough. Is the buyer asking for the Exchange MRAID tag described by the current specification, or a different account workflow that must be verified?

  1. Name the product and delivery route. Include placement, operating systems, orientations, destination URL, account owner, and buyer or network contact.
  2. Load the dated export profile. Every volatile claim needs a source URL, a verification date, and an explicit status.
  3. Build the route artifact. Apply the selected packager, runtime bridge, lifecycle policy, and byte accounting to the shared source.
  4. Validate the final bytes. Test the artifact after all wrapping, encoding, metadata injection, and compression—not the project folder.
  5. Freeze the evidence. Record the artifact hash, profile version, validation result, devices, and unresolved questions with the handoff.

Host mandate is stated by the cited platform. Vendor recommendation is advised by that platform but is not presented here as a universal acceptance rule. Hookin control is a conservative engineering choice. Keeping those labels visible prevents a useful safety margin from becoming fake platform law.

The cross-route matrix

This matrix was source-checked on 2026-07-29. “Unknown” is a release state, not an invitation to copy a number from another row.

Delivery route Artifact and size contract Runtime and host contract Validation evidence and date Open question
google_ads_app_campaign_zip Host mandate: ZIP up to 5 MB and 512 files in the main App campaign specification. General remote references are restricted, with documented Google-hosted exceptions. Hookin control: warn at 4.5 MB compressed ZIP. Declare portrait, landscape, or both. Audio waits for interaction. Custom HTML can use ExitApi.exit(); Google also acknowledges an MRAID-equivalent exit path. No required MRAID version is published for this route. The asset requirements page links an HTML5 validator. The upload preview is sandboxed and intentionally blocks outbound CTA navigation. Verified 2026-07-29. The troubleshooting page says 5.2 MB while the main page says 5 MB. Exact media extensions and the current validator workflow still need a live upload check.
applovin_single_html_mraid2 Host mandate: exactly one HTML file, maximum 5 MB, assets embedded with base64 or base122, and no external calls. Hookin control: warn at 4.5 MB final HTML. MRAID 2.0; both orientations. Timer and audio wait for the first interaction, hidden audio stops or mutes, and neither automatic nor first-tap store redirect is allowed. CTA uses mraid.open(). Use AppLovin's web preview and mobile Preview apps; its page explicitly says desktop success does not guarantee device success. Verified 2026-07-29. The preview's Runtime Analysis link currently leads to an ambiguous moved destination; confirm the current required procedure with AppLovin.
unity_ads_ua_single_html_mraid3 Host mandate: one file named index.html, all assets inline, no other files or folders, under 5 MB. Network requests are generally prohibited except a narrow lawful, non-personal analytics allowance. MRAID 3.0; both orientations. Wait for MRAID readiness and viewability. Do not redirect automatically or on first touch; use mraid.open(). Unity provides a hosted-URL and QR workflow through its Ad Testing app. Verified 2026-07-29. Unity's moderation checks say under 6 MB while its current consolidated specification says under 5 MB. Use the stricter under-5-MB contract.
unity_exchange_mraid3 Programmatic creative or tag contract. Do not inherit the Unity Ads UA one-file uploader rules or its ceiling without transaction evidence. Unity Exchange documents MRAID 3.0, readiness behavior, and a supported-method subset. Its compatibility surface is the contract, not the similarly named UA uploader. No standalone public creative validator was verified in the research. Capture account or buyer integration evidence. Verified 2026-07-29. Package, remote-origin, size, and commercial constraints can be transaction-specific and require account confirmation.
ironsource_exchange_mraid2_tag Host mandate: raw HTML and JavaScript snippets, not a complete document; maximum 4 MB across downloaded code and assets. Absolute HTTPS assets are allowed; relative assets are not. Hookin control: warn at 3.6 MB predicted transfer. MRAID 2.0. The creative should autoplay, use mraid.getMaxSize(), and exit with mraid.open(). The host supplies timer and close controls. Both orientations are a vendor recommendation. No standalone public validator was verified. Test in the account or buyer-specific integration and capture its result. Verified 2026-07-29. Confirm whether the actual handoff is this Exchange tag route; never use “ironSource” or “LevelPlay” as an automatic alias.
meta_playable_ads_account_verified Status: account-verification-required. Package shape, size measurement, file count, entry point, and remote-origin policy remain unknown in this matrix. CTA bridge, MRAID version, readiness, audio, lifecycle, and orientation are also unknown. No limit or API is borrowed from a secondary guide. The official format page, technical page, and create/test page were login-gated during verification on 2026-07-29. An authorized operator must record the current help content and upload UI before this profile can release a production artifact.

Read the Google row with both sources open

Google is a good test of whether the matrix preserves evidence or merely stores numbers. Its main playable specification says 5 MB; its troubleshooting page says 5.2 MB for an HTML5 interstitial. Choosing 5.1 MB because it sits between the two is not reconciliation. The defensible export decision is to enforce the stricter official figure and keep a separate Hookin warning threshold of 4.5 MB.

The same restraint applies to remote origins. “No network requests” is easy to remember but inaccurate for Google: the official pages permit named Google-hosted libraries and Google Fonts while restricting arbitrary external references. An all-local default can still be a sensible Hookin control; it just must not be described as Google's exact allowlist.

Finally, a CTA that does not navigate inside Google's documented sandbox preview is not, by itself, evidence of a broken exit. The preview intentionally blocks outbound navigation. The export record should distinguish “bridge call observed in the sandbox” from “destination opened in the final network or device test.”

Keep Unity's three surfaces in three boxes

Unity Ads user acquisition, Unity Exchange, and ironSource Exchange are three routes, even when one organization owns the documentation. Unity Ads UA wants one complete inlined index.html, under 5 MB, MRAID 3.0, both orientations, and a start gated by viewability. Its official device path is the Unity Ad Testing app.

Unity Exchange is a programmatic MRAID 3.0 environment with a published supported-method surface. The public page does not give this article permission to paste the UA uploader's package and size rules into the exchange row.

ironSource Exchange goes the other direction: its official contract is an MRAID 2.0 snippet, allows absolute HTTPS assets, measures up to 4 MB across downloaded code and assets, and expects autoplay. It also supplies timer and close controls. A build system that merges these three rows will eventually emit an artifact that is valid for none of them.

Share source state, then adapt the edges

The creative core should own what makes the experience itself recognizable: the deterministic game state, input semantics, mechanic rules, source art and audio, layout constraints, copy, and measurement-neutral events such as tutorial_completed or cta_requested. It can also own graceful rendering fallbacks as long as route policy decides when they run.

The route layer owns the things the host can disagree about: artifact topology, byte accounting, remote origins, metadata, MRAID version, DOM and host readiness, visibility, sizing, audio unlock, hidden-state behavior, first-touch eligibility, CTA bridge, host chrome, and validator path.

SOURCE CORE
  mechanic + state machine + source art + layout intent
                         |
                  route selector
                         |
        +----------------+----------------+
        |                |                |
    packager       runtime adapter    policy profile
   ZIP / HTML /    ready / visible /  bytes / origins /
   snippet         audio / exit       orientation / UI
        +----------------+----------------+
                         |
                 FINAL ROUTE ARTIFACT
                         |
             official preview + device evidence

This boundary does not guarantee that every source feature belongs on every route. It makes the exception visible. If remote streaming is essential to the mechanic, an AppLovin or Unity Ads UA profile should fail the build instead of silently changing its network policy. If a route has no verified CTA bridge, as in the current Meta row, the adapter remains blocked rather than guessing.

Turn the matrix into a build contract

A spreadsheet is useful for reading; the exporter needs a schema it can reject. The JSON Schema below requires every contract field, allows only six route IDs, and wraps each value with its evidence status, direct sources, verification date, and open question. A blank unknown is invalid; represent it as a null value with unverified status.

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "Playable export profile",
  "type": "object",
  "additionalProperties": false,
  "required": ["route_id", "source_verified_date", "claims"],
  "properties": {
    "route_id": {
      "enum": [
        "google_ads_app_campaign_zip",
        "applovin_single_html_mraid2",
        "unity_ads_ua_single_html_mraid3",
        "unity_exchange_mraid3",
        "ironsource_exchange_mraid2_tag",
        "meta_playable_ads_account_verified"
      ]
    },
    "source_verified_date": {
      "type": "string",
      "format": "date"
    },
    "claims": {
      "type": "object",
      "required": [
        "package_kind",
        "size_limit_bytes",
        "size_limit_comparison",
        "size_unit_policy",
        "size_measurement",
        "max_file_count",
        "required_entrypoint",
        "allowed_remote_origins",
        "orientation_contract",
        "mraid_version",
        "ready_gate",
        "visibility_gate",
        "audio_unlock",
        "hidden_audio_policy",
        "first_touch_exit_allowed",
        "cta_bridge",
        "official_preview",
        "required_controls",
        "prohibited_controls",
        "webgl_fallback",
        "known_document_conflicts"
      ],
      "propertyNames": {
        "enum": [
          "package_kind",
          "size_limit_bytes",
          "size_limit_comparison",
          "size_unit_policy",
          "size_measurement",
          "max_file_count",
          "required_entrypoint",
          "allowed_remote_origins",
          "orientation_contract",
          "mraid_version",
          "ready_gate",
          "visibility_gate",
          "audio_unlock",
          "hidden_audio_policy",
          "first_touch_exit_allowed",
          "cta_bridge",
          "official_preview",
          "required_controls",
          "prohibited_controls",
          "webgl_fallback",
          "known_document_conflicts"
        ]
      },
      "allOf": [
        {
          "properties": {
            "size_limit_bytes": {
              "$ref": "#/$defs/nonnegative_integer_claim"
            },
            "size_limit_comparison": {
              "$ref": "#/$defs/boundary_claim"
            },
            "size_unit_policy": {
              "$ref": "#/$defs/nonempty_string_claim"
            },
            "max_file_count": {
              "$ref": "#/$defs/nonnegative_integer_claim"
            },
            "first_touch_exit_allowed": {
              "$ref": "#/$defs/boolean_claim"
            },
            "required_controls": {
              "$ref": "#/$defs/control_list_claim"
            },
            "prohibited_controls": {
              "$ref": "#/$defs/control_list_claim"
            },
            "webgl_fallback": {
              "$ref": "#/$defs/boolean_claim"
            }
          }
        }
      ],
      "additionalProperties": {
        "$ref": "#/$defs/claim"
      }
    }
  },
  "$defs": {
    "claim": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "status",
        "value",
        "source_urls",
        "verified_date",
        "open_question"
      ],
      "properties": {
        "status": {
          "enum": [
            "official",
            "vendor_recommendation",
            "hookin_policy",
            "unverified"
          ]
        },
        "value": {},
        "source_urls": {
          "type": "array",
          "minItems": 1,
          "uniqueItems": true,
          "items": {
            "type": "string",
            "format": "uri",
            "pattern": "^https://"
          }
        },
        "verified_date": {
          "type": "string",
          "format": "date"
        },
        "open_question": {
          "anyOf": [
            {
              "type": "string",
              "minLength": 1
            },
            {
              "type": "null"
            }
          ]
        }
      },
      "allOf": [
        {
          "if": {
            "properties": {
              "status": {
                "const": "unverified"
              }
            },
            "required": ["status"]
          },
          "then": {
            "properties": {
              "value": {
                "type": "null"
              },
              "open_question": {
                "type": "string",
                "minLength": 1
              }
            }
          },
          "else": {
            "properties": {
              "value": {
                "not": {
                  "type": "null"
                }
              }
            }
          }
        }
      ]
    },
    "nonnegative_integer_claim": {
      "allOf": [
        {
          "$ref": "#/$defs/claim"
        },
        {
          "properties": {
            "value": {
              "type": ["integer", "null"],
              "minimum": 0
            }
          }
        }
      ]
    },
    "boundary_claim": {
      "allOf": [
        {
          "$ref": "#/$defs/claim"
        },
        {
          "properties": {
            "value": {
              "enum": ["strict_less_than", "inclusive", null]
            }
          }
        }
      ]
    },
    "nonempty_string_claim": {
      "allOf": [
        {
          "$ref": "#/$defs/claim"
        },
        {
          "properties": {
            "value": {
              "type": ["string", "null"],
              "minLength": 1
            }
          }
        }
      ]
    },
    "boolean_claim": {
      "allOf": [
        {
          "$ref": "#/$defs/claim"
        },
        {
          "properties": {
            "value": {
              "type": ["boolean", "null"]
            }
          }
        }
      ]
    },
    "control_list_claim": {
      "allOf": [
        {
          "$ref": "#/$defs/claim"
        },
        {
          "properties": {
            "value": {
              "type": ["array", "null"],
              "minItems": 1,
              "uniqueItems": true,
              "items": {
                "type": "string",
                "minLength": 1
              }
            }
          }
        }
      ]
    }
  }
}

The schema does not decide the values. It prevents a build profile from forgetting the questions. size_limit_comparison preserves whether a boundary is strict or inclusive, while size_unit_policy records the decimal-or-binary byte conversion instead of hiding it in exporter code. A route can contain official claims, vendor recommendations, Hookin controls, and unverified fields at the same time. For Google, 5 MB is official, 4.5 MB is a Hookin warning policy, and accepted packaged media extensions still carry an open verification question. Flattening those into one unlabeled number destroys the reasoning.

The extension fields are release gates, not a notes drawer. An AppLovin profile should record webgl_fallback as an official true claim because its creative guidance requires a graceful fallback when WebGL initialization or context restoration fails. An ironSource Exchange profile should put loading_screen in required_controls and place top_window_access, blocking_third_party_pixels, and single_line_html_comments in prohibited_controls, following the cited MRAID integration contract. Package checks and runtime-policy checks remain separate claims even when both can block release.

A profile is only as current as its evidence

Put verification dates on claims, not only on the article. Platform documentation changes asynchronously, and two official pages can disagree. Each profile revision should retain the exact source URLs, visible revision date when available, verification date, account and role for gated evidence, artifact hash, validation message or screenshot, and the person who resolved or reopened the question.

Use official only when the cited host documentation mandates that field. Use vendor_recommendation when the same source advises a practice without making it an acceptance condition. Use hookin_policy for a stricter threshold, adapter normalization, or release control derived from the official contract. Use unverified when the source is inaccessible, contradictory without a safe stricter choice, or specific to an account integration that has not been inspected.

That distinction is especially important for Meta. The honest profile currently contains more null values than the other rows. It should remain blocked until an authorized operator records the current technical help and upload UI. Unknown does not make Meta unimportant; it makes account verification part of the export job.

Make the export decision before polish is finished

Route selection belongs near the start of production, not in the upload hour. It changes whether assets can stay remote, how much base64 expansion matters, which host signals start the game, what safe space the layout needs, and how the CTA is tested. The playable-ad primer explains the format; this matrix owns the narrower engineering question of how one approved experience becomes a defensible delivery artifact.

The practical release sequence is short: select one exact route ID, resolve every unverified field that can block it, build from the shared source, measure the final artifact in the route's unit, run the official preview and real-device path available for that route, and freeze the evidence with the file. If the same campaign needs another route, run the sequence again with another profile.

Keep the mechanic shared. Keep the art system shared. Keep the state machine shared. But let the ZIP, HTML, snippet, MRAID bridge, byte ledger, lifecycle rules, and validation record tell the truth about where the playable is actually going.

Back to blog

Keep reading

Turn the idea into a playable

Build and test an interactive ad in Hookin. No code required.

Start free