Integrating bidding in your mobile application from the server (server to server bidding)

Facebook Audience Network has an ORTB bidder which competes for app, mobile web, and video impressions; either directly from the client (actual mobile devices), or from a server (auction platform). We support both direct publisher integrations and mediated integrations. This guide describes how you can integrate with bidding in your mobile app from your in-house auction server.

This document will walk you through a sample integration with bidding on the server side. Make sure that you have read the document on ORTB bidding to understand bidding before you start the integration.

Here is an overview of the workflow that we will be covering:

server-to-server bidding with in-house auction server - your waterfall is managed on your in-house auction server

In this guide, we will be covering how the client app can send an pre-defined auction request to the auction server, and the auction server will use that to create the required bid request for Audience Network. Then the auction server will select the best bid and return it to the app. Finally the app will load the ad based on response from the auction server.

The sample code we provide is only for demonstrating how the bidding API works and how you can integrate it with your in-house auction server. The sample auction server is written in Python with Flask, with minimal stability and security considerations for the simplicity of the source code.

In the step by step guide below, we will be using the example of bidding on an interstitial ad. Make sure that you are already familiar with using Audience Network interstitial ads. Bidding also supports Native, Banner, Interstitial, In-stream Video and Rewarded Video formats. When you are using ad formats other than interstitial for bidding, you can change the corresponding ORTB request parameters and the SDK classes to load those ads. For more details please refer to the sample server app.

Step-by-Step

1. Server - Defining the platforms on the server

2. Client - Making an auction request

3. Server - Making the ORTB requests

4. Server - Running the auction

5. Client - Loading the ad

1. Server - Defining the platforms on the server

The ORTB request required by Audience Network bidding endpoint contains both information about the device and information about the application. On your own auction server, you can define some of the application specifics in the server configuration so that your client side app only need to send device specs and app identification for the server to construct the actual ORTB request.

Here is a sample ORTB request. For more detailed documentation on the ORTB request please refer to the document on ORTB bidding:

{
      // Device information from client side
      'device': {
          'ifa': 'E6460824-EC16-4657-841C-6684C60CEF5A',
          'dnt': '0',
          'ip': '127.0.0.1'
      },
      // Application information
      'app': {
          'ver': '1.0',
          'bundle': 'com.facebook.audiencenetwork.AdUnitsSample',
          'publisher': {
              // For server to server bidding integration, this is your application id on Facebook
              'id': '256699801203835',
          }
      },
      // Placement information we can store on server
      'imp': [
          {
              'id': 'banner_test_bid_req_id',
              // This is the placement id for Audience Network
              'tagid': '256699801203835_326140227593125',
              'banner': {
                  'w': -1,
                  'h': 50,
              },
          },
      ],
      // Optional regulations object from client side
      'regs': {
          'coppa': 0,
      },
      // In server to server integration, you can use the Facebook app id as platform id here
      'ext': {
          'platformid': 256699801203835,
      },
      // buyeruid is the user token generated on client side, using the `getBidderToken` method from the Audience Network SDK. 
      // It's constant through out app session so you could cache it on the client side
      'user': {
          'buyeruid': 'mybuyeruid',
      },
      // Test mode flag
      'test': '1',
      // Time out setting we can store on server
      'tmax': 1000,
      // Request ID we can generate on server
      'id': 'banner_test_bid_req_id',
      // Auction setting we can store on server
      'at': 1,
  }

In the example above, we can store Audience Network related information, including app ID, placement ID, and parameters for different placement type on the server. The client app only needs to send a simplified request including the device specs, and the server can help look up the placement information and construct the ORTB request. In our sample app, here is a sample auction request sent from the client to the auction server:

{
    // App ID and placement ID are used to look up settings from
    // server settings
    'app_id': '101',
    'placement_id': '1',

    'bundle': 'com.facebook.samples.s2sbiddingclient',
    'bundle_version': '1.0',

    // Device specifics
    'ifa': '51bdd958-eb07-42eb-ae63-ced1384813e3',
    'coppa': 0,
    'dnt': 0,

    // buyer_tokens are the user tokens required for different networks
    'buyer_tokens': {
      // Token for audience network from BidderTokenProvider.getBidderToken(context)
      // This can be cached for the same app session
      'audience_network': 'my-buyeruid',
    },
}

Below is the sample auction server settings. The auction server will be able to take the parameters in the auction request above and look up the corresponding placement information and construct the final ORTB request.

{
  "bidding_source_platforms": [
    {
      "platform_name": "audience_network",
      "end_point": "https://an.facebook.com/placementbid.ortb",
      "timeout": 1000,
      "timeout_notification_url": "https://www.facebook.com/audiencenetwork/nurl/?partner=${PARTNER_FBID}&app=${APP_FBID}&auction=${AUCTION_ID}&ortb_loss_code=2"
    }
  ],
  "apps": [
    {
      "app_id": "101",
      "app_name": "My example app",
      "placements": [
        {
          "placement_id": "1",
          "placement_name": "My example placement",
          "ad_format": "interstitial",
          "bidding_source_placement_ids":[
            {
              "platform_name": "audience_network",
              "platform_app_id": "175610266242921",
              "platform_placement_id": "175610266242921_175733672897247"
            }
          ]
        }
      ]
    }
  ]
}

Finally our sample server can create this response to the client to notify which platform to use:

{
  'placement_id': string, // Placement identifier for the auction server
  'ad_format': string, // Format of the placement
  'platform_name': string, // Which platform won the auction, for example 'audience_network'
  'platform_placement_id': string, // Placement ID for the platform, for example the placement ID for Audience network
  'bid_payload': string, // The JSON string payload for the platform SDK to load the final ad
}

2. Client - Making an auction request

On the client side, we need to gather the required parameters for the auction request, and send it to the auction server using HTTP request. Here is an example implementation in Android for making the auction request specified by the format above:

private static JSONObject getRequest(
        Context context,
        String appId,
        String placementId,
        String idfa,
        int coppa,
        int dnt
) throws JSONException {
    JSONObject requestObject = new JSONObject();
    requestObject.put("app_id", appId);
    requestObject.put("placement_id", placementId);

    requestObject.put("bundle", BuildConfig.APPLICATION_ID);
    requestObject.put("bundle_version", BuildConfig.VERSION_NAME);

    requestObject.put("ifa", idfa);

    // coppa and do not track flags
    requestObject.put("coppa", coppa);
    requestObject.put("dnt", dnt);

    requestObject.put(
            "buyer_tokens",
            (new JSONObject())
                    .put("audience_network", getAudienceNetworkBidderToken(context))
    );

    requestObject.put("test", Settings.isTestMode() ? 1 : 0);

    return requestObject;
}

Here is the sample code snippet of a static function that sends the auction request to the server, and invoke the callback methods with the server response:

public static void makeRequest(
        final Context context,
        final int coppa,
        final String appId,
        final String placementId,
        final AuctionRequestCallback callback
) {
    getThreadPoolExecutor().submit(new Runnable() {
        @Override
        public void run() {
            try {
                // Retrieve idfa and limit ad tracking settings
                NonProductionIDFAUtils.AdIdInfo adInfo = NonProductionIDFAUtils.getAdIdInfo(context);

                final JSONObject request = getRequest(
                        context,
                        appId,
                        placementId,
                        adInfo.idfa,
                        coppa,
                        adInfo.dnt);

                if (Settings.getServerAddress() == null) {
                    throw new android.provider.Settings.SettingNotFoundException(
                            "Server address settings not found");
                }
                final URL url = new URL(Settings.getServerAddress());

                JsonObjectRequest jsObjRequest = new JsonObjectRequest(
                        Request.Method.POST,
                        Settings.getServerAddress(),
                        request,
                        new Response.Listener<JSONObject>() {
                            @Override
                            public void onResponse(JSONObject response) {
                                try {
                                    Log.d("response", response.toString());

                                    String placementId = response.getString("placement_id");
                                    String adFormat = response.getString("ad_format");

                                    String platformName = response.getString("platform_name");
                                    String platformPlacementId = response.getString(
                                            "platform_placement_id");
                                    String payload = response.getString("bid_payload");

                                    callback.onSuccess(
                                            placementId,
                                            adFormat,
                                            platformName,
                                            platformPlacementId,
                                            payload);

                                } catch (JSONException e) {
                                    callback.onError(e);
                                }
                            }
                        },
                        new Response.ErrorListener() {
                            @Override
                            public void onErrorResponse(VolleyError error) {
                                if (error.networkResponse == null) {
                                    callback.onError(new Exception("Server response empty!"));
                                } else {
                                    if(error.networkResponse.data != null) {
                                        callback.onError(new VolleyError(
                                                error.networkResponse.statusCode
                                                        + " " +
                                                        new String(error.networkResponse.data)));
                                    } else {
                                        callback.onError(new VolleyError("" +
                                                error.networkResponse.statusCode));
                                    }
                                }
                            }
                        }
                );

                getRequestQueue(context).add(jsObjRequest);

            } catch (Exception e) {
                // Other errors
                Log.e("error", e.toString());
                callback.onError(e);
            }
        }
    });
}

3. Server - Making the ORTB requests

After the auction server receives the auction request, it needs to construct the bid requests for each of the platforms. The auction request from the client contains the app_id and placement_id which will be used to look up for the settings in the server's settings. With these platform specific settings, here is the sample snippet that generates a bid request for Audience Network platform:

def get_bid_request(
    ip,
    user_agent,
    auction_id,
    platform_app_id,
    platform_placement_id,
    ad_format,
    request_params
):
    '''
    Gather the required bid request parameters for networks. The parameters
    consist of platform settings like app id, placement ids, ad sizes etc., and
    client side information such as device information, user agent etc. We use
    the `settings.json` file to store platform specific settings, and the
    client request to retrieve the clietn specific information.
    '''
    platform = ServerSettings.get_platform('audience_network')
    end_point = platform['end_point']
    timeout = platform['timeout']
    timeout_notification_url = platform['timeout_notification_url']

    timeout_notification_url.replace('${PARTNER_FBID}', platform_app_id)
    timeout_notification_url.replace('${APP_FBID}', platform_app_id)
    timeout_notification_url.replace('${AUCTION_ID}', auction_id)

    imp = []
    if ad_format == 'native':
        imp.append({
            'id': auction_id,
            'native': {
                'w': -1,
                'h': -1,
            },
            'tagid': platform_placement_id,
        })
    elif ad_format == 'banner':
        imp.append({
            'id': auction_id,
            'banner': {
                'w': -1,
                'h': 50,
            },
            'tagid': platform_placement_id,
        })
    elif ad_format == 'interstitial':
        imp.append({
            'id': auction_id,
            'banner': {
                'w': 0,
                'h': 0,
            },
            'tagid': platform_placement_id,
            'instl': 1,
        })
    elif ad_format == 'rewarded_video':
        imp.append({
            'id': auction_id,
            'video': {
                'w': 0,
                'h': 0,
                'linearity': 2,
            },
            'tagid': platform_placement_id,
        })
    elif ad_format == 'instream_video':
        imp.append({
            'id': auction_id,
            'video': {
                'w': 0,
                'h': 0,
                'linearity': 1,
            },
            'tagid': platform_placement_id,
        })
    else:
        raise ParameterError("Incorrect ad format")

    typed_ip = ipaddress.ip_address(ip)
    device = {
        'ifa': request_params['ifa'],
        'ua': user_agent,
        'dnt': request_params['dnt'],
    }
    if type(typed_ip) is ipaddress.IPv6Address:
        device['ipv6'] = ip
    else:
        device['ip'] = ip

    # Construct the ORTB request
    request = {
        'id': auction_id,
        'imp': imp,
        'app': {
            'bundle': request_params['bundle'],
            'ver': request_params['bundle_version'],
            'publisher': {
                'id': platform_app_id,
            }
        },
        'device': device,
        'regs': {
            'coppa': request_params['coppa'],
        },
        'user': {
            'buyeruid': request_params['buyer_tokens']['audience_network'],
        },
        'ext': {
            'platformid': platform_app_id,
        },
        'at': 1,
        'tmax': timeout,
        'test': request_params['test'],
    }

    return (end_point, request, timeout_notification_url)

After the ORTB request object is created above, we can send the request to the Audience Network endpoint at http://an.facebook.com/placementbid.ortb using HTTP request, using post and Content-Type: application/json. In the response, no-bids are returned as an HTTP 204 error code. The following HTTP headers (on both bids and no-bids) will be set that contain useful information for troubleshooting and should be logged on the auction server:

  • X-FB-AN-Request-ID: The Request ID is needed for Audience Network to debug a specific request. Please capture it whenever asking for support.
  • X-FB-AN-Errors: A list of errors encountered, useful to understand reasons for no-bids.
  • X-FB-Debug: Some debug information about this request that you can send to your account representative at Audience Network for troubleshooting.

Here is a sample snippet that send the ORTB request to Audience Network:

def exec_bid_request(
    end_point,
    request_params,
    timeout_notification_url
):
    try:
        platform = ServerSettings.get_platform('audience_network')
        timeout = platform['timeout']
        r = requests.post(
            end_point,
            json=request_params,
            timeout=(timeout / 2000),
        )
    except Exception as e:
        current_app.logger.error(BID_TIMEOUT)

        # Send time out notification
        r = requests.get(timeout_notification_url, timeout)
        return (500, BID_TIMEOUT)

    if r.status_code == requests.codes.ok:
        try:
            data = json.loads(r.text)
            current_app.logger.debug('Audience Network response: {}'.format(
                data
            ))
            # Parse response from Audience Network with the ORTBResponse
            ortb_response = ORTBResponse(data)
        except Exception as e:
            current_app.logger.error(
                PARSE_ERROR + "{}".format(e)
            )
            return (500, PARSE_ERROR + "{}".format(e))

        return (r.status_code, ortb_response)

    else:
        # The error message is stored in the X-FB-AN-Errors header
        error_header = r.headers.get('x-fb-an-errors')
        debug_header = r.headers.get('x-fb-debug')
        bid_request_id = r.headers.get('x-fb-an-request-id')

        if r.status_code == 400:
            error_message = INVALID_BID + error_header + INVALID_BID_ADVICE
        elif r.status_code == 204:
            error_message = NO_BID + error_header
        else:
            error_message = UNEXPECTED_ERROR

        # Log error information for debugging
        error = {
            'bid_request_id': bid_request_id,
            'debug_header': debug_header,
            'error_message': error_message,
        }
        current_app.logger.error(error)

        # Respond error status code to client
        return (r.status_code, error_message)

4. Server - Running the auction

Here is the high level code for the auction server:

  • Firstly it find the placement information in the server settings
  • Then with the settings and the request data it creates the bid requests for each platform
  • Then it executes the bid requests for each of the platform (this part can be eventually implemented using parallel threads to reduce latency of the auction server)
  • Then it compares the filled bid responses and decide on the highest bid
def get_bid_response(ip, user_agent, request_params):
  """Get the winner bid response for the current request"""
  try:
      app_id = request_params['app_id']
      placement_id = request_params['placement_id']

      auction_id = get_auction_id()

      current_app.logger.debug(
          'Running auction {0} for app {1} placement {2}'.format(
              auction_id,
              app_id,
              placement_id,
          )
      )

      # Find placement in the settings
      placement = ServerSettings.get_placement(app_id, placement_id)

      bid_requests = get_bid_requests(
          ip,
          user_agent,
          auction_id,
          placement,
          request_params)

  except Exception as e:
      raise ParameterError("Error in request parameters: {}".format(str(e)))

  # Execute bid requests by network
  bid_responses = []
  for bid_request in bid_requests:
      (code, response) = exec_bid_request(
          bid_request['platform_name'],
          bid_request['end_point'],
          bid_request['data'],
          bid_request['timeout_notification_url'])

      bid_responses.append({
          'platform_name': bid_request['platform_name'],
          'code': code,
          'response': response,
      })

  final_response = run_auction(bid_responses, placement)
  return final_response

Because Audience Network is the only bidder in our sample, the run auction method simply compares the returned bid with some price value and decide if it wins the auction. As a result, if the bid returned from Audience Network is higher than 1 dollar, we respond as Audience Network has won the bid, otherwise we treat it as it lost the auction.

Win notifications

When we know the result of the auction, we should send a win/loss notification to Audience Network. The win notification url is nurl in Audience Network's bid response, the lose notification url is the lurl in the bid response, and you should replace ${AUCTION_LOSS} in the url with the specific loss reason code:

EventDescriptionORTB v2.5 Loss Reason

Invalid bid response

Bid is invalid (but on-time, not a no-bid, and valid enough that you can extract the nurl)

3

Bid timeout *

Bid response received, but too late for auction cutoff

2

No bid

No-bids are indicated as HTTP 204 (i.e. no nurl to call), but you may interpret our response as a no-bid (likely an integration issue). You may also request bids for several impressions, and we bid on some but not all.

9

Not highest RTB bidder

Another bidder beat us, including synthetic bids (e.g. non-RTB exchanges), if they are entered into the same auction.

102

Inventory didn't materialise

Our bid won the auction, but the impression didn't materialize (e.g. page wasn't long enough to include this slot, or the user exited the app before the cached ad was used.) Not all partners can provide this (it's a non-event), so we will infer it if not provided.

4902

Sent to ad server

Send this if the last touchpoint you have with the decision process is sending our high bid to the ad server. The impression may still be lost through missing line items, the ad server overruling the auction, or the inventory not materializing.

4900

RTB winner not picked by ad server

We won the RTB auction, but the ad server overruled the auction (e.g. direct).

4903

Win

We won the full decision tree, and tag was placed on page (web) or ad object was cached (app). Viewable impression may still not result.

0

Timeout lurl

For the case of bid timeout, we provide you with an alternative reporting route. It is the generic lurl which might be called upon without the need to wait for the bid to arrive. The format is as follows:

"https://www.facebook.com/audiencenetwork/nurl/?partner=${PARTNER_FBID}&app=${APP_FBID}&auction=${AUCTION_ID}&ortb_loss_code=2"
ParamTypeDescription

PARTNER_FBID

Int

Ad auction partner id issued by Facebook. Use your app id here if you don't have a dedicated ad auction partner.

APP_FBID

Int

Facebook-issued Id of the application which initiated an auction.

AUCTION_ID

String

Client-generated id of the auction you used for issuing a bid request.

Here is the sample code that compares Audience Network bid price with 1 dollar to decide if it wins the auction:

def run_auction(bid_responses, placement):
  """Run auction based on raw responses and create the response object"""
  other_bid = 1  # some price to compare against
  response = (204, None)  # default is 204 no fill

  for bid_response in bid_responses:
      if bid_response['platform_name'] == 'audience_network':
          if bid_response['code'] == 200:
              ortb_response = bid_response['response']
              if ortb_response.price > other_bid:
                  response = create_response(bid_response, placement)
                  current_app.logger.debug(
                      'Audience Network bid: {} won!'.format(
                          ortb_response.price
                      )
                  )
                  notify_result(bid_response)
              else:
                  current_app.logger.debug(
                      'Audience Network bid: {} lost!'.format(
                          ortb_response.price
                      )
                  )
                  notify_result(bid_response, 102)
          else:
              current_app.logger.debug(bid_response['response'])

  return response

After running the auction, we create the response object and return it to the client app. Here is the method in the sample that creates the final response of the auction server:

def create_response(bid_response, placement):
    """Create response object based on the auction result"""
    ad_format = placement['ad_format']
    platform_name = bid_response['platform_name']
    platform_placement_id = None

    for bidding_source_placement_id in placement[
        'bidding_source_placement_ids'
    ]:
        if bidding_source_placement_id['platform_name'] == platform_name:
            platform_placement_id = bidding_source_placement_id[
                'platform_placement_id'
            ]

    if platform_placement_id is None:
        raise InternalError("Platform placement ID not found!")

    bid_payload = None
    if platform_name == 'audience_network':
        bid_payload = bid_response['response'].adm
    else:
        raise InternalError("Invalid platform")

    return (200, {
        'placement_id': placement['placement_id'],
        'ad_format': ad_format,
        'platform_name': platform_name,
        'platform_placement_id': platform_placement_id,
        'bid_payload': bid_payload,
    })

5. Client - Loading the ad

On the client app, when the HTTP request returns, the callback methods we specified above will be called. Using the response parameters, we can tell which platform won the auction, and load the ad. If the auction was successful and Audience Network won the auction, the response parameters will contain a payload string which we can load the ad from. We can call the loadAdFromBid method on the correct ad format class to load the ad. Here are the implemented auction request callback methods:

// AuctionRequestCallback
public void onSuccess(
        String placementId,
        String adFormat,
        String platformName,
        String platformPlacementId,
        String payload) {

    if (platformName.contentEquals(PLATFORM_AUDIENCE_NETWORK)) {
        if (adFormat.contentEquals(INTERSTITIAL)) {
            statusLabel.setText(R.string.loading_ad);

            interstitialAd = new InterstitialAd(MainActivity.this, platformPlacementId);
            interstitialAd.setAdListener(MainActivity.this);
            interstitialAd.loadAdFromBid(payload);
        }
    }
}

public void onError(Exception e) {
    Log.e(TAG, e.getMessage());
    statusLabel.setText(e.getMessage());
}

As a result, the sample app will load the ad if Audience Network won the bid, or display the error message received from the auction server.