Pulse OpenRTB integration details

Connections

  • Pulse supports and we strongly recommend employing the OpenRTB best practice of using persistent connections, the default option is HTTP 1.1.
  • Pulse keeps multiple connections to a DSP open in parallel. The total number can roughly be calculated by: <agreed queries per second> / <max response time in ms allowed> * 10.

    For example, if the agreed QPS is 5000 and the max allowed response time is 100ms, there would be 500 connections open in parallel.

Basic auction overview

  1. Pulse receives an ad request from a viewer’s device.
  2. Pulse identifies if any bid requests can be made to DSPs for this particular impression.
  3. Pulse makes an HTTP POST request to each DSP’s bidder HTTP endpoint. DSPs that do not respond within 100ms are timed out.
  4. Invalid bids (for example, not matching the creative specification, blocked advertisers, and so on) are filtered out.
  5. Pulse then runs one out of two types of auctions:
    1. Fixed price (for direct deals), for example:
      • Fixed price is $32.0
      • Buyer A (the only one allowed) bids $32.0
      • The fixed price becomes the settlement price = $32.0
    2. 2nd price auctions for private marketplaces
      In 2nd price auctions, the settlement price is derived as one of the following four options:
      1. The highest bid
      2. 0.01 increment from the 2nd highest bid
      3. 0.01 increment from the floor price
      4. The floor price

      For example:

      Floor price Bidder A Bidder B Settlement price Explanation
      $20.0 $25.0 $25.0 $25.0 Two bidders are bidding at the same price, which is higher than the floor price. The bid that was received first is the winning bid.
      $20.0 $25.0 $21.5 $21.51 Bidder A wins and pays $0.01 increment from the 2nd highest bid because the 2nd highest bid is higher than the floor price.
      $20.0 $20.0 $25.0 $20.01 Bidder B wins and pays $0.01 increment from the floor price because the 2nd highest bid matches the floor price.
      $20.0 $18.0 $21.5 $20.0 Bidder B wins and pays the floor price because its bid is the only valid one.
      $20.0 $20.0 $20.0 $20.0 Two bidders are bidding at the floor price. The bid that was received first is the winning bid.
      $20.0 $25.0 - $20.0 Bidder A pays the floor price because it is the only one bidding.
  6. The winning creative is served to the viewer’s device, and a Win Notice is called to notify the winning bidder (see Win notice below for details).

Win notice

Server-to-Server

Pulse supports server-to-server win notice. The win notice is triggered when the auction has finished and the bid was elected as winner. However, this does not necessarily mean that the ad gets selected by our holistic ad decisioning engine. We encourage encryption in order to exchange price information. Pulse takes the following approach to inform the DSP about the winning bid:
  1. If both bid.adm and bid.nurl are present:
    1. Replace supported macros in bid.adm and bid.nurl
    2. Trigger bid.nurl
    3. Serve bid.adm to the viewer’s device
  2. If only bid.adm is present:
    1. Expect the win notification to be embedded in the bid.adm impression URLs
    2. Replace supported macros in bid.adm
    3. Serve bid.adm to the viewer’s device
  3. If only bid.nurl is present:
    1. Replace supported macros in bid.nurl
    2. Trigger bid.nurl and expect the response body to contain ad markup. There must be no other structured data in the response body.
    3. Replace supported macros in the retrieved ad markup
    4. Serve the retrieved ad markup from win notice call to the viewer’s device

Impression life cycle

Impression life cycle

Seat taxonomy

This refers to what the DSPs should send in the bid response.

Direct DSP integrations

When the integration is done directly with a DSP, the seat IDs are those agreed directly with the DSP in question. The DSP is requested to send over a seat mapping table to indicate which seat IDs exist and which buyers are behind each seat.

Switchboard-type integrations (Indirect DSP integrations)

When the integration is with a switchboard type setup, meaning that the integrating platform does not represent the actual DSP, but routes traffic to several other DSPs, the seat taxonomy should be one of the following:
  • <SWITCHBOARD DSP ID>: a seller can target all buyers behind a specific DSP
  • <SWITCHBOARD DSP ID>.<DSP SEAT ID>: a seller can target a specific buyer behind a specific DSP
  • No seat taxonomy required: seller can target all buyers behind all DSPs, which means that a bid from any seat is allowed

User ID synchronisation (Cookie matching)

In order for the DSP to know which user a bid request relates to, a user ID sync (commonly referred to as Cookie Matching) needs to happen. Pulse hosts a matching table over the DSP’s user IDs to be sent over in bid requests.

Enable cookie sync

The following procedure is employed to sync the IDs:

  1. DSP provides Pulse with a user sync URL.
  2. Pulse calls the user sync URL from the viewer’s device if the viewer/user is not already synced with the DSP or if the sync information is more than 15 days old.
  3. The URL redirects back to Pulse with the DSP’s user ID attached.
  4. Pulse updates the browser cookie with the DSP’s user ID and a fresh timestamp.
Note: Both HTTP and HTTPS are supported.

Example

User sync URL, dsp.com/sync?ssp=invidi, redirects to Pulse user ID sync endpoint, ssp.videoplaza.tv/proxy/sync?dsp=INVIDI_PROVIDED_DSP_ID&uid=DSP_USER_ID.

Supported macros

Pulse supports the following substitution macros for both the bid.adm and bid.nurl fields:

Macro Description
${AUCTION_ID} ID of the bid request; from BidRequest.id attribute.
${AUCTION_BID_ID} ID of the bid; from BidResponse.bidid attribute.
${AUCTION_IMP_ID} ID of the impression just won; from imp.id attribute.
${AUCTION_SEAT_ID} ID of the bidder seat for whom the bid was made.
${AUCTION_AD_ID} ID of the ad markup the bidder wishes to serve; from bid.adid attribute.
${AUCTION_PRICE:X} Settlement price using the same currency and units as the bid. X, if present, specifies the method of encryption.
${AUCTION_CURRENCY} The currency used in the bid (explicit or implied); for confirmation only.

Encryption

HMAC & RC4

Use the macro ${AUCTION_PRICE:RC4} to use this encryption.

Encryption details

  • ekey = encryption key (32 bytes)
  • ikey = integrity key (32 bytes)
  • message = the message to encode
  • HMAC(k,d) = SHA-1 HMAC digest of input data d using key k
  • RC4(k,d) = arcfour streamcipher of input data d using key k
  • a || b = concatenation of strings a and b
  • head(s,n) = string containing the heading n characters of string s
  • base64(d) = base64 encoded representation of data d using websafe alphabet

How to encode

{
    digest = HMAC ( ekey, ikey || msg )
    signature = head ( digest, 8 )
    cipher = RC4 ( ekey, signature || msg )
    encoded_data = base64 ( cipher )
}

How to decode (Java example)

private static final String HASH_ALGORITHM = "HmacSHA1";

public String decrypt(String message) {
  byte[] m = org.apache.commons.codec.binary.Base64.decodeBase64(message);
  try {
     byte[] ekeyBytes = ekey.getBytes(CHARSET_ENCODING);
     final byte[] decrypted = new RC4(ekeyBytes).encrypt(m);
     final byte[] hmac = hmac(ekeyBytes, concat(ikey.getBytes(),
Arrays.copyOfRange(decrypted, 8, decrypted.length)));

     boolean equal = true;
     for (int i = 0; i < 8; i++)
        if (decrypted[i] != hmac[i]) equal = false;
     if (!equal)
        throw new IllegalArgumentException("Invalid signature, message not authentic.");
     return new String(decrypted, 8, decrypted.length - 8);
  } catch (UnsupportedEncodingException e) {
     throw new RuntimeException(e);
  } catch (GeneralSecurityException e) {
     throw new RuntimeException(e);
  }
}

private byte[] hmac(byte[] key, byte[] message) throws NoSuchAlgorithmException,
InvalidKeyException, UnsupportedEncodingException {
  Mac mac = Mac.getInstance(HASH_ALGORITHM);
  mac.init(new SecretKeySpec(key, HASH_ALGORITHM));
  return mac.doFinal(message);
}

Example keys and messages for verification:

@Test
public void testDecryption() {
  final Decryptor decryptor = new Decryptor().withIKey("lJIWHudSXJ03JOba6DBavlIiWOxON7FR").withEKey
("OcTBKYWAOxnpl8r7eugVm59guVsJUH0g");
  assertEquals("1234567890", decryptor.decrypt("h3niKQYzYNBG-G4JJI0hARp7"));
  assertEquals("12", decryptor.decrypt("FDDY9YyadMNG-A"));
  assertEquals("12.41", decryptor.decrypt("MbEMFDgQeS5G-HMJIA"));
  assertEquals("1012", decryptor.decrypt("BAHNiUYeXNRG-mwP"));
  assertEquals("1.0011", decryptor.decrypt("TdrjSYRRPyJG5G0NIIo"));
  assertEquals("1234", decryptor.decrypt("g-uum32m9s1G-G4J"));
}