S2S Transaction Validator

Overview

Server-to-Server (S2S) Transaction Validator allows your backend to send purchase data that has already been validated with Apple App Store or Google Play directly to Magify.

Use this integration when purchase validation is performed by your backend or by a third-party service other than RevenueCat.

In this mode, Magify trusts the purchase data delivered by your server and skips its own store-side validation.


Configuration

Step 1. Request S2S Access in Magify

Server-to-Server access is provisioned only through Magify technical support.

Contact support to request S2S access for your application. They will provide:

  • the S2S endpoint URL;
  • the Authorization Code required to authenticate your server requests.

Step 2. Configure Your Server

After your server validates a purchase with Apple App Store or Google Play, build the payload described below and deliver it to Magify.

The complete request — including the HTTP method, endpoint URL, and required headers — is described in the Sending the payload section later in this article.


Request Fields

The request payload contains four groups of fields:

  • Identity
  • Transaction
  • Money
  • Classification

Identity

The following fields identify the user and the application associated with the purchase.

Transaction

The following fields describe the purchase transaction.

Money

The following fields describe the financial details of the purchase.

Classification

The following fields classify the purchase for reporting purposes.

Example Payload

The following example shows the smallest valid payload. It includes all required fields and omits all optional fields.

If optional fields are omitted, the following default values are applied:

  • currencyUSD
  • environmentPRODUCTION
  • commission_amount0
  • is_trial_conversionfalse

Although type, period_type, and product_id_type are optional, we recommend always sending them in production because they are used to classify purchases in reports.

{
  "client_id": "00000000-0000-0000-0000-000000000001",
  "app_version": "1.0",
  "country": "US",
  "product_id": "com.example.coins_pack_small",
  "transaction_id": "2000000812345678",
  "original_transaction_id": "2000000812345678",
  "purchased_at": 1747310000.22,
  "price": 9.99,
  "store_name": "NATIVE"
}

Purchase Scenarios

The following examples demonstrate the recommended field combinations for common purchase scenarios.

The fields type, period_type, is_trial_conversion, and product_id_type are validated independently. Any valid combination is accepted by the endpoint. Reporting accuracy therefore depends on your server sending the values that correspond to the actual purchase event.

The following fields are common to every example:

  • client_id, app_version, and country identify the user.
  • purchased_at is a Unix timestamp in seconds.
  • store_name is NATIVE for Apple App Store and Google Play purchases.

Free Trial Started

The user entered a trial and has not been charged yet.

Send price: 0 because the trial itself does not generate revenue.

{
 "client_id": "00000000-0000-0000-0000-000000000001",
 "app_version": "1.0",
 "country": "US",
 "product_id": "com.example.sub.weekly",
 "transaction_id": "2000000900000001",
 "original_transaction_id": "2000000900000001",
 "purchased_at": 1747310000,
 "price": 0,
 "currency": "USD",
 "store_name": "NATIVE",
 "storefront": "US",
 "type": "INITIAL_PURCHASE",
 "period_type": "TRIAL",
 "is_trial_conversion": false,
 "product_id_type": "subscription_product_id",
 "environment": "PRODUCTION"
}

Trial Converted to a Paid Subscription

The first paid charge after the trial ends.

This purchase is identified by:

  • is_trial_conversion: true
  • original_transaction_id referencing the transaction that started the trial
{
 "client_id": "00000000-0000-0000-0000-000000000001",
 "app_version": "1.0",
 "country": "US",
 "product_id": "com.example.sub.weekly",
 "transaction_id": "2000000900000002",
 "original_transaction_id": "2000000900000001",
 "purchased_at": 1747914800,
 "price": 9.99,
 "currency": "USD",
 "commission_amount": 1.5,
 "commission_currency": "USD",
 "store_name": "NATIVE",
 "storefront": "US",
 "type": "RENEWAL",
 "period_type": "NORMAL",
 "is_trial_conversion": true,
 "product_id_type": "subscription_product_id",
 "environment": "PRODUCTION"
}

Regular One-off Purchase

A consumable or other non-renewing product purchased at full price.

For one-off purchases, original_transaction_id is equal to transaction_id.

{
 "client_id": "00000000-0000-0000-0000-000000000001",
 "app_version": "1.0",
 "country": "US",
 "product_id": "com.example.coins_pack_small",
 "transaction_id": "2000000812345678",
 "original_transaction_id": "2000000812345678",
 "purchased_at": 1747310000.22,
 "price": 9.99,
 "currency": "USD",
 "commission_amount": 1.5,
 "commission_currency": "USD",
 "store_name": "NATIVE",
 "storefront": "US",
 "type": "NON_RENEWING_PURCHASE",
 "period_type": "NORMAL",
 "is_trial_conversion": false,
 "product_id_type": "consumable_product_id",
 "environment": "PRODUCTION"
}

Test Purchase

Used during integration and internal QA.

Use:

  • type: TEST
  • environment: SANDBOX

These fields are independent. A sandbox transaction that should also be excluded from revenue must include both values.

{
 "client_id": "00000000-0000-0000-0000-000000000001",
 "app_version": "1.0",
 "country": "US",
 "product_id": "com.example.coins_pack_small",
 "transaction_id": "2000000899999999",
 "original_transaction_id": "2000000899999999",
 "purchased_at": 1747310000,
 "price": 9.99,
 "currency": "USD",
 "store_name": "NATIVE",
 "storefront": "US",
 "type": "TEST",
 "period_type": "NORMAL",
 "is_trial_conversion": false,
 "product_id_type": "consumable_product_id",
 "environment": "SANDBOX"
}

Gifted Product

A product granted by your backend without a native store purchase.

A gift is identified by price: 0. Any product can be granted as a gift, so product_id_type must still match the type configured for the product in Magify.

In practice, most gifted products are configured as one of the following types:

  • bonus_product_id — A bonus granted by the game, such as compensation, a starter gift, or an in-game reward.
  • rewarded_product_id — A product granted for watching a rewarded ad or completing another rewarded action.
  • cross_promo_product_id — A product granted through a cross-promotion.

Because there is no native store transaction, generate your own stable transaction_id (a UUID is recommended) and reuse it as original_transaction_id.

The identifier must remain stable for the same gift so that retrying the request does not create duplicate purchases.

{
  "client_id": "00000000-0000-0000-0000-000000000001",
  "app_version": "1.0",
  "country": "US",
  "product_id": "com.example.starter_gift",
  "transaction_id": "9f1c2f6e-5f0b-4a67-9d3f-2c9b6a71e401",
  "original_transaction_id": "9f1c2f6e-5f0b-4a67-9d3f-2c9b6a71e401",
  "purchased_at": 1747310000,
  "price": 0,
  "currency": "USD",
  "store_name": "NATIVE",
  "type": "NON_RENEWING_PURCHASE",
  "period_type": "NORMAL",
  "is_trial_conversion": false,
  "product_id_type": "bonus_product_id",
  "environment": "PRODUCTION"
}

Sending the Payload

Send the JSON payload as the body of an HTTP POST request to the S2S endpoint URL provided for your application.

The endpoint has the following format:

https://webhook.magify.com/v1/s2s/{app_slug}

{app_slug} is the slug of your application in Magify—a short, URL-safe identifier assigned to the application (for example, my-game), not a UUID.

The same endpoint is used for both sandbox and production traffic. The transaction environment is determined by the environment field in the payload.

The endpoint URL and the bearer token are provided by Magify technical support during Step 1.

The request must include:

  • Method: POST
  • URL: https://webhook.magify.com/v1/s2s/{app_slug}
  • Content-Type: application/json
  • Authorization: Bearer <token> — Use the bearer token provided by Magify technical support. Requests without this header, or with an invalid token, are rejected.

Example curl request:

curl -X POST "https://webhook.magify.com/v1/s2s/{app_slug}" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer {bearer_token}" \
  -d '{ "app_version": "1.0", "client_id": "00000000-0000-0000-0000-000000000001", ... }'

Use "environment": "SANDBOX" while testing and "environment": "PRODUCTION" for live traffic.

The endpoint URL remains the same. Magify determines the environment from the environment field in the payload.


Step 3. Update the Client SDK

Although validated purchases are delivered to Magify through S2S, the client SDK must still send analytics events, such as purchase started, purchase completed, and subscription events.

To complete the S2S setup, set skipVerification to true for every PurchaseInfo you create. When this flag is enabled, the Magify SDK skips its own validation flow and relies entirely on the purchase payload delivered by your server.

How to Set the Flag

PurchaseInfo is typically created in your purchase callback — most commonly inside the method that implements Unity IAP's IStoreListener.ProcessPurchase, or inside the Magify store wrappers IMinimalInAppStore and IInAppStore.

Pass the flag through the constructor:

PurchaseProcessingResult IStoreListener.ProcessPurchase(PurchaseEventArgs e)
{
    var product = e.purchasedProduct;

    // skipVerification: true — Magify will NOT call its validation service.
    // Your server is responsible for sending the validated purchase via S2S.
    var purchaseInfo = new PurchaseInfo(
        product,
        LoadSubscriptionInfo(product.definition.id),
        skipVerification: true);

    if (purchaseInfo.SubscriptionInfo != null)
        OnSubscriptionPurchaseFinished?.Invoke(product.definition.id, purchaseInfo);

    OnPurchaseFinished?.Invoke(product.definition.id, purchaseInfo);

    return PurchaseProcessingResult.Complete;
}

If only some products use S2S validation (for example, purchases from an external store), set the flag only for those products and leave the default behavior for all other purchases.

Align Client Events with the S2S Payload

The identifiers sent in the S2S request must match the identifiers sent by the client SDK for analytics, regardless of whether skipVerification is enabled.

Make sure the following values are identical on both the client and server:

  • client_id — If your application assigns its own client identifier to MagifyManager, send exactly the same value in the S2S payload. Otherwise, read the SDK-generated identifier from MagifyService.Instance.ClientId and forward it to your backend.
  • transaction_id / original_transaction_id — Use the transaction identifiers provided by the native store (transactionIdentifier on Apple App Store and orderId on Google Play) and forward them to your backend without modification.
  • product_id — Use the same store product identifier that was used on the client.

Mismatched identifiers may split a purchase across multiple users or create duplicate purchase records.

Hybrid Setup

If only some of your purchases are validated externally (for example, through a proprietary web shop in addition to Apple App Store or Google Play), you can continue using Magify validation for native purchases while using the trusted purchase flow for external purchases.

Populate a TrustedPurchaseRecord on PurchaseInfo.

See External Validation for complete client-side integration examples, including trusted purchases, Server-to-Server validation, and hybrid setups.


Step 4. Send a Test Transaction

  1. From your server, send a test purchase payload to the S2S endpoint using "environment": "SANDBOX" and a sandbox transaction.
  2. In Magify, open Transaction Validator for your application and verify that the test transaction appears in the validation log.
  3. After the test is successful, change "environment" to "PRODUCTION" for live traffic. The endpoint URL and bearer token remain the same.

Related articles

Pangle (TikTok Audience Network)

LevelPlay

Tapjoy

Gamelight

User Acquisition (UA) Cost Tracking

Almedia