├── .editorconfig ├── .github └── workflows │ └── test.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── audio.go ├── audio_test.go ├── banner.go ├── banner_test.go ├── bench_test.go ├── bid.go ├── bid_test.go ├── bidrequest.go ├── bidrequest_test.go ├── bidresponse.go ├── bidresponse_test.go ├── content.go ├── content_test.go ├── device.go ├── device_test.go ├── doc.go ├── go.mod ├── go.sum ├── impression.go ├── impression_test.go ├── inventory.go ├── inventory_test.go ├── native.go ├── native ├── request │ ├── asset.go │ ├── data.go │ ├── image.go │ ├── request.go │ ├── request_test.go │ ├── testdata │ │ └── request1.json │ ├── title.go │ └── video.go └── response │ ├── asset.go │ ├── data.go │ ├── image.go │ ├── link.go │ ├── response.go │ ├── response_test.go │ ├── testdata │ ├── response1.json │ └── response2.json │ ├── title.go │ └── video.go ├── native_test.go ├── numbers.go ├── numbers_test.go ├── openrtb.go ├── openrtb_test.go ├── pmp.go ├── pmp_test.go ├── quantity.go ├── quantity_test.go ├── seatbid.go ├── seatbid_test.go ├── source.go ├── source_test.go ├── sua.go ├── testdata ├── app.json ├── audio.json ├── banner.json ├── bid.json ├── breq.banner.json ├── breq.exp.json ├── breq.native.json ├── breq.video.json ├── bres.multi.json ├── bres.pmp.json ├── bres.single.json ├── bres.vast.json ├── content.json ├── content.quoted.json ├── device.json ├── impression.json ├── native.json ├── pmp.json ├── quantity.json ├── site.json ├── source.json └── video.json ├── video.go └── video_test.go /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.{json,rb,md,yml,yaml,feature}] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | jobs: 10 | go: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | go-version: [1.18.x, 1.19.x] 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: actions/setup-go@v3 18 | with: 19 | go-version: ${{ matrix.go-version }} 20 | cache: true 21 | - run: make test 22 | golangci: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v3 26 | - uses: actions/setup-go@v3 27 | with: 28 | go-version: 1.x 29 | cache: true 30 | - uses: golangci/golangci-lint-action@v3 31 | with: 32 | version: latest 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.makefile 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Black Square Media Ltd. All rights reserved. 2 | (The MIT License) 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | 'Software'), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 19 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 20 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 21 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | Some test examples were taken from: 24 | https://code.google.com/p/openrtb/wiki/OpenRTB_Examples 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | default: test 2 | 3 | .minimal.makefile: 4 | curl -fsSL -o $@ https://gitlab.com/bsm/misc/raw/master/make/go/minimal.makefile 5 | 6 | include .minimal.makefile 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenRTB 2 | 3 | [](https://travis-ci.org/bsm/openrtb) 4 | 5 | OpenRTB structs and validations for Go. 6 | 7 | ## Requirements 8 | 9 | Requires Go 1.8+ for proper `json.RawMessage` marshaling. 10 | 11 | ## Installation 12 | 13 | To install, use `go get`: 14 | 15 | ```shell 16 | go get github.com/bsm/openrtb/v3 17 | ``` 18 | 19 | ## Usage 20 | 21 | ```go 22 | package main 23 | 24 | import ( 25 | "log" 26 | "github.com/bsm/openrtb/v3" 27 | ) 28 | 29 | func main() { 30 | file, err := os.Open("stored.json") 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | defer file.Close() 35 | 36 | var req *openrtb.BidRequest 37 | if err := json.NewDecoder(file).Decode(&req); err != nil { 38 | log.Fatal(err) 39 | } 40 | 41 | log.Printf("%+v\n", req) 42 | } 43 | ``` 44 | -------------------------------------------------------------------------------- /audio.go: -------------------------------------------------------------------------------- 1 | package openrtb 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | ) 7 | 8 | // Validation errors 9 | var ( 10 | ErrInvalidAudioNoMIMEs = errors.New("openrtb: audio has no mimes") 11 | ) 12 | 13 | // Audio object must be included directly in the impression object 14 | type Audio struct { 15 | MIMEs []string `json:"mimes"` // Content MIME types supported. 16 | MinDuration int `json:"minduration,omitempty"` // Minimum video ad duration in seconds 17 | MaxDuration int `json:"maxduration,omitempty"` // Maximum video ad duration in seconds 18 | Protocols []Protocol `json:"protocols,omitempty"` // Video bid response protocols 19 | StartDelay StartDelay `json:"startdelay,omitempty"` // Indicates the start delay in seconds 20 | Sequence int `json:"sequence,omitempty"` // Default: 1 21 | BlockedAttrs []CreativeAttribute `json:"battr,omitempty"` // Blocked creative attributes 22 | MaxExtended int `json:"maxextended,omitempty"` // Maximum extended video ad duration 23 | MinBitrate int `json:"minbitrate,omitempty"` // Minimum bit rate in Kbps 24 | MaxBitrate int `json:"maxbitrate,omitempty"` // Maximum bit rate in Kbps 25 | Delivery []ContentDelivery `json:"delivery,omitempty"` // List of supported delivery methods 26 | CompanionAds []Banner `json:"companionad,omitempty"` 27 | APIs []APIFramework `json:"api,omitempty"` 28 | CompanionTypes []CompanionType `json:"companiontype,omitempty"` 29 | MaxSequence int `json:"maxseq,omitempty"` // The maximumnumber of ads that canbe played in an ad pod. 30 | Feed FeedType `json:"feed,omitempty"` // Type of audio feed. 31 | Stitched int `json:"stitched,omitempty"` // Indicates if the ad is stitched with audio content or delivered independently 32 | VolumeNorm VolumeNorm `json:"nvol,omitempty"` // Volume normalization mode. 33 | Ext json.RawMessage `json:"ext,omitempty"` 34 | } 35 | 36 | type jsonAudio Audio 37 | 38 | // Validate the object 39 | func (a *Audio) Validate() error { 40 | if len(a.MIMEs) == 0 { 41 | return ErrInvalidAudioNoMIMEs 42 | } 43 | return nil 44 | } 45 | 46 | // MarshalJSON custom marshalling with normalization 47 | func (a *Audio) MarshalJSON() ([]byte, error) { 48 | a.normalize() 49 | return json.Marshal((*jsonAudio)(a)) 50 | } 51 | 52 | // UnmarshalJSON custom unmarshalling with normalization 53 | func (a *Audio) UnmarshalJSON(data []byte) error { 54 | var h jsonAudio 55 | if err := json.Unmarshal(data, &h); err != nil { 56 | return err 57 | } 58 | 59 | *a = (Audio)(h) 60 | a.normalize() 61 | return nil 62 | } 63 | 64 | func (a *Audio) normalize() { 65 | if a.Sequence == 0 { 66 | a.Sequence = 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /audio_test.go: -------------------------------------------------------------------------------- 1 | package openrtb_test 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "testing" 7 | 8 | . "github.com/bsm/openrtb/v3" 9 | ) 10 | 11 | func TestAudio(t *testing.T) { 12 | var subject *Audio 13 | if err := fixture("audio", &subject); err != nil { 14 | t.Fatalf("expected no error, got %v", err) 15 | } 16 | 17 | exp := &Audio{ 18 | MIMEs: []string{ 19 | "audio/mp4", 20 | }, 21 | MinDuration: 5, 22 | MaxDuration: 30, 23 | Protocols: []Protocol{ProtocolDAAST1, ProtocolDAAST1Wrapper}, 24 | Sequence: 1, 25 | BlockedAttrs: []CreativeAttribute{CreativeAttributeUserInitiated, CreativeAttributeWindowsDialogOrAlert}, 26 | MaxExtended: 30, 27 | MinBitrate: 300, 28 | MaxBitrate: 1500, 29 | Delivery: []ContentDelivery{ContentDeliveryProgressive}, 30 | CompanionAds: []Banner{ 31 | {Width: 300, Height: 250, ID: "1234567893-1", Position: AdPositionAboveFold, BlockedAttrs: []CreativeAttribute{CreativeAttributeUserInitiated, CreativeAttributeWindowsDialogOrAlert}, ExpDirs: []ExpDir{ExpDirRight, ExpDirDown}}, 32 | {Width: 728, Height: 90, ID: "1234567893-2", Position: AdPositionAboveFold, BlockedAttrs: []CreativeAttribute{CreativeAttributeUserInitiated, CreativeAttributeWindowsDialogOrAlert}}, 33 | }, 34 | APIs: []APIFramework{APIFrameworkVPAID1, APIFrameworkVPAID2}, 35 | CompanionTypes: []CompanionType{CompanionTypeStatic, CompanionTypeHTML}, 36 | } 37 | if got := subject; !reflect.DeepEqual(exp, got) { 38 | t.Errorf("expected %+v, got %+v", exp, got) 39 | } 40 | } 41 | 42 | func TestAudio_Validate(t *testing.T) { 43 | subject := &Audio{ 44 | MinDuration: 5, 45 | MaxDuration: 30, 46 | Protocols: []Protocol{ProtocolDAAST1, ProtocolDAAST1Wrapper}, 47 | Sequence: 1, 48 | BlockedAttrs: []CreativeAttribute{CreativeAttributeUserInitiated, CreativeAttributeWindowsDialogOrAlert}, 49 | MaxExtended: 30, 50 | MinBitrate: 300, 51 | MaxBitrate: 1500, 52 | Delivery: []ContentDelivery{ContentDeliveryProgressive}, 53 | CompanionAds: []Banner{ 54 | {Width: 300, Height: 250, ID: "1234567893-1", Position: AdPositionAboveFold, BlockedAttrs: []CreativeAttribute{CreativeAttributeUserInitiated, CreativeAttributeWindowsDialogOrAlert}, ExpDirs: []ExpDir{ExpDirRight, ExpDirDown}}, 55 | {Width: 728, Height: 90, ID: "1234567893-2", Position: AdPositionAboveFold, BlockedAttrs: []CreativeAttribute{CreativeAttributeUserInitiated, CreativeAttributeWindowsDialogOrAlert}}, 56 | }, 57 | CompanionTypes: []CompanionType{CompanionTypeStatic, CompanionTypeHTML}, 58 | } 59 | if exp, got := ErrInvalidAudioNoMIMEs, subject.Validate(); !errors.Is(exp, got) { 60 | t.Fatalf("expected %v, got %v", exp, got) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /banner.go: -------------------------------------------------------------------------------- 1 | package openrtb 2 | 3 | import "encoding/json" 4 | 5 | // Banner object must be included directly in the impression object if the impression offered 6 | // for auction is display or rich media, or it may be optionally embedded in the video object to 7 | // describe the companion banners available for the linear or non-linear video ad. The banner 8 | // object may include a unique identifier; this can be useful if these IDs can be leveraged in the 9 | // VAST response to dictate placement of the companion creatives when multiple companion ad 10 | // opportunities of the same size are available on a page. 11 | type Banner struct { 12 | Width int `json:"w,omitempty"` // Width 13 | Height int `json:"h,omitempty"` // Height 14 | Formats []Format `json:"format,omitempty"` // Array of format objects representing the banner sizes permitted. 15 | WidthMax int `json:"wmax,omitempty"` // Width maximum DEPRECATED 16 | HeightMax int `json:"hmax,omitempty"` // Height maximum DEPRECATED 17 | WidthMin int `json:"wmin,omitempty"` // Width minimum DEPRECATED 18 | HeightMin int `json:"hmin,omitempty"` // Height minimum DEPRECATED 19 | ID string `json:"id,omitempty"` // A unique identifier 20 | BlockedTypes []BannerType `json:"btype,omitempty"` // Blocked banner types 21 | BlockedAttrs []CreativeAttribute `json:"battr,omitempty"` // Blocked creative attributes 22 | Position AdPosition `json:"pos,omitempty"` // Ad Position 23 | MIMEs []string `json:"mimes,omitempty"` // Whitelist of content MIME types supported 24 | TopFrame int `json:"topframe,omitempty"` // Default: 0 ("1": Delivered in top frame, "0": Elsewhere) 25 | ExpDirs []ExpDir `json:"expdir,omitempty"` // Specify properties for an expandable ad 26 | APIs []APIFramework `json:"api,omitempty"` // List of supported API frameworks 27 | VCM int `json:"vcm,omitempty"` // Represents the relationship with video. 0 = concurrent, 1 = end-card 28 | Ext json.RawMessage `json:"ext,omitempty"` 29 | } 30 | -------------------------------------------------------------------------------- /banner_test.go: -------------------------------------------------------------------------------- 1 | package openrtb_test 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | . "github.com/bsm/openrtb/v3" 8 | ) 9 | 10 | func TestBanner(t *testing.T) { 11 | var subject *Banner 12 | if err := fixture("banner", &subject); err != nil { 13 | t.Fatalf("expected no error, got %v", err) 14 | } 15 | 16 | exp := &Banner{ 17 | Width: 728, 18 | Height: 90, 19 | Position: AdPositionAboveFold, 20 | BlockedTypes: []BannerType{BannerTypeFrame}, 21 | BlockedAttrs: []CreativeAttribute{CreativeAttributeWindowsDialogOrAlert}, 22 | APIs: []APIFramework{APIFrameworkMRAID1}, 23 | VCM: 1, 24 | } 25 | if got := subject; !reflect.DeepEqual(exp, got) { 26 | t.Errorf("expected %+v, got %+v", exp, got) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /bench_test.go: -------------------------------------------------------------------------------- 1 | package openrtb 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | ) 9 | 10 | func BenchmarkBidRequest_Unmarshal(b *testing.B) { 11 | data, err := os.ReadFile(filepath.Join("testdata", "breq.video.json")) 12 | if err != nil { 13 | b.Fatal(err.Error()) 14 | } 15 | 16 | b.ResetTimer() 17 | for i := 0; i < b.N; i++ { 18 | var req *BidRequest 19 | if err := json.Unmarshal(data, &req); err != nil { 20 | b.Fatal(err.Error()) 21 | } 22 | } 23 | } 24 | 25 | func BenchmarkBidRequest_Marshal(b *testing.B) { 26 | data, err := os.ReadFile(filepath.Join("testdata", "breq.video.json")) 27 | if err != nil { 28 | b.Fatal(err.Error()) 29 | } 30 | 31 | var req *BidRequest 32 | if err := json.Unmarshal(data, &req); err != nil { 33 | b.Fatal(err.Error()) 34 | } 35 | 36 | b.ResetTimer() 37 | for i := 0; i < b.N; i++ { 38 | if _, err := json.Marshal(req); err != nil { 39 | b.Fatal(err.Error()) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /bid.go: -------------------------------------------------------------------------------- 1 | package openrtb 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | ) 7 | 8 | // Validation errors 9 | var ( 10 | ErrInvalidBidNoID = errors.New("openrtb: bid is missing ID") 11 | ErrInvalidBidNoImpID = errors.New("openrtb: bid is missing impression ID") 12 | ) 13 | 14 | // Bid object contains bid information. 15 | // ID, ImpID and Price are required; all other optional. 16 | // If the bidder wins the impression, the exchange calls notice URL (nurl) 17 | // a) to inform the bidder of the win; 18 | // b) to convey certain information using substitution macros. 19 | // Adomain can be used to check advertiser block list compliance. 20 | // Cid can be used to block ads that were previously identified as inappropriate. 21 | // Substitution macros may allow a bidder to use a static notice URL for all of its bids. 22 | type Bid struct { 23 | ID string `json:"id"` 24 | ImpID string `json:"impid"` // Required string ID of the impression object to which this bid applies. 25 | Price float64 `json:"price"` // Bid price in CPM. Suggests using integer math for accounting to avoid rounding errors. 26 | AdID string `json:"adid,omitempty"` // References the ad to be served if the bid wins. 27 | NoticeURL string `json:"nurl,omitempty"` // Win notice URL. 28 | BillingURL string `json:"burl,omitempty"` // Billing notice URL. 29 | LossURL string `json:"lurl,omitempty"` // Loss notice URL. 30 | AdMarkup string `json:"adm,omitempty"` // Actual ad markup. XHTML if a response to a banner object, or VAST XML if a response to a video object. 31 | AdvDomains []string `json:"adomain,omitempty"` // Advertiser’s primary or top-level domain for advertiser checking; or multiple if imp rotating. 32 | Bundle string `json:"bundle,omitempty"` // A platform-specific application identifier intended to be unique to the app and independent of the exchange. 33 | ImageURL string `json:"iurl,omitempty"` // Sample image URL. 34 | CampaignID StringOrNumber `json:"cid,omitempty"` // Campaign ID that appears with the Ad markup. 35 | CreativeID string `json:"crid,omitempty"` // Creative ID for reporting content issues or defects. This could also be used as a reference to a creative ID that is posted with an exchange. 36 | Tactic string `json:"tactic,omitempty"` // Tactic ID to enable buyers to label bids for reporting to the exchange the tactic through which their bid was submitted. 37 | Categories []ContentCategory `json:"cat,omitempty"` // IAB content categories of the creative. Refer to List 5.1 38 | Attrs []CreativeAttribute `json:"attr,omitempty"` // Array of creative attributes. 39 | API APIFramework `json:"api,omitempty"` // API required by the markup if applicable, NOTE: for ORTB ver <= 2.5 APIFramework supported is 1 to 6. 40 | Protocol Protocol `json:"protocol,omitempty"` // Video response protocol of the markup if applicable 41 | MediaRating IQGRating `json:"qagmediarating,omitempty"` // Creative media rating per IQG guidelines. 42 | Language string `json:"language,omitempty"` // Language of the creative using ISO-639-1-alpha-2. 43 | DealID string `json:"dealid,omitempty"` // DealID extension of private marketplace deals 44 | Width int `json:"w,omitempty"` // Width of the ad in pixels. 45 | Height int `json:"h,omitempty"` // Height of the ad in pixels. 46 | WidthRatio int `json:"wratio,omitempty"` // Relative width of the creative when expressing size as a ratio. 47 | HeightRatio int `json:"hratio,omitempty"` // Relative height of the creative when expressing size as a ratio. 48 | 49 | APIS APIFramework `json:"apis,omitempty"` // APIS required by the markup if applicable. 50 | LangB string `json:"langb,omitempty"` // Language of the creative using IETF BCP 47. Only one of language or langb should be present. 51 | Duration int `json:"dur,omitempty"` // Duration of the video or audio creative in seconds. 52 | MarkupType MarkupType `json:"mtype,omitempty"` // Creative markup so that it can properly be associated. 53 | SlotInPod SlotPositionInPod `json:"slotinpod,omitempty"` // Indicates that the bid response is only eligible for a specific position. 54 | CategoryTaxonomy CategoryTaxonomy `json:"cattax,omitempty"` // Defines the taxonomy in use. 55 | 56 | Exp int `json:"exp,omitempty"` // Advisory as to the number of seconds the bidder is willing to wait between the auction and the actual impression. 57 | Ext json.RawMessage `json:"ext,omitempty"` 58 | } 59 | 60 | // Validate required attributes 61 | func (bid *Bid) Validate() error { 62 | if bid.ID == "" { 63 | return ErrInvalidBidNoID 64 | } else if bid.ImpID == "" { 65 | return ErrInvalidBidNoImpID 66 | } 67 | 68 | return nil 69 | } 70 | -------------------------------------------------------------------------------- /bid_test.go: -------------------------------------------------------------------------------- 1 | package openrtb_test 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "testing" 7 | 8 | . "github.com/bsm/openrtb/v3" 9 | ) 10 | 11 | func TestBid(t *testing.T) { 12 | var subject *Bid 13 | if err := fixture("bid", &subject); err != nil { 14 | t.Fatalf("expected no error, got %v", err) 15 | } 16 | 17 | exp := &Bid{ 18 | ID: "1", 19 | ImpID: "1", 20 | Price: 0.751371, 21 | AdID: "52a5516d29e435137c6f6e74", 22 | NoticeURL: "http://ads.com/win/112770_1386565997?won=${AUCTION_PRICE}", 23 | AdMarkup: "
", 24 | AdvDomains: []string{"ads.com"}, 25 | ImageURL: "http://ads.com/112770_1386565997.jpeg", 26 | CampaignID: "52a5516d29e435137c6f6e74", 27 | CreativeID: "52a5516d29e435137c6f6e74_1386565997", 28 | DealID: "example_deal", 29 | Attrs: []CreativeAttribute{}, 30 | } 31 | if got := subject; !reflect.DeepEqual(exp, got) { 32 | t.Errorf("expected %+v, got %+v", exp, got) 33 | } 34 | } 35 | 36 | func TestBid_Validate(t *testing.T) { 37 | subject := &Bid{} 38 | if exp, got := ErrInvalidBidNoID, subject.Validate(); !errors.Is(exp, got) { 39 | t.Fatalf("expected %v, got %v", exp, got) 40 | } 41 | subject = &Bid{ID: "BIDID"} 42 | if exp, got := ErrInvalidBidNoImpID, subject.Validate(); !errors.Is(exp, got) { 43 | t.Fatalf("expected %v, got %v", exp, got) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /bidrequest.go: -------------------------------------------------------------------------------- 1 | package openrtb 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | ) 7 | 8 | // Validation errors 9 | var ( 10 | ErrInvalidReqNoID = errors.New("openrtb: request ID missing") 11 | ErrInvalidReqNoImps = errors.New("openrtb: request has no impressions") 12 | ErrInvalidReqMultiInv = errors.New("openrtb: request has multiple inventory sources") // has site and app 13 | ) 14 | 15 | // BidRequest is the top-level bid request object contains a globally unique bid request or auction ID. This "id" 16 | // attribute is required as is at least one "imp" (i.e., impression) object. Other attributes are 17 | // optional since an exchange may establish default values. 18 | type BidRequest struct { 19 | ID string `json:"id"` // Unique ID of the bid request 20 | Impressions []Impression `json:"imp,omitempty"` 21 | Site *Site `json:"site,omitempty"` 22 | App *App `json:"app,omitempty"` 23 | Device *Device `json:"device,omitempty"` 24 | User *User `json:"user,omitempty"` 25 | Test int `json:"test,omitempty"` // Indicator of test mode in which auctions are not billable, where 0 = live mode, 1 = test mode 26 | AuctionType int `json:"at"` // Auction type, where 1 = First Price, 2 = Second Price Plus. Exchange-specific auction types can be defined using values greater than 500. 27 | TimeMax int `json:"tmax,omitempty"` // Maximum amount of time in milliseconds to submit a bid 28 | Seats []string `json:"wseat,omitempty"` // Array of buyer seats allowed to bid on this auction 29 | BlockedSeats []string `json:"bseat,omitempty"` // Array of buyer seats blocked to bid on this auction 30 | Languages []string `json:"wlang,omitempty"` // Allowed list of languages for creatives using ISO-639-1-alpha-2. Omission implies no specific restrictions, but buyers would be advised to consider language attribute in the Device and/or Content objects if available. Only one of wlang or wlangb should be present. 31 | LanguagesB []string `json:"wlangb,omitempty"` // Allowed list of languages for creatives using IETF BCP 47I. Omission implies no specific restrictions, but buyers would be advised to consider language attribute in the Device and/or Content objects if available. Only one of wlang or wlangb should be present. 32 | AllImpressions int `json:"allimps,omitempty"` // Flag to indicate whether exchange can verify that all impressions offered represent all of the impressions available in context, Default: 0 33 | Currencies []string `json:"cur,omitempty"` // Array of allowed currencies 34 | BlockedCategories []ContentCategory `json:"bcat,omitempty"` // Blocked Advertiser Categories. 35 | BlockedAdvDomains []string `json:"badv,omitempty"` // Array of strings of blocked toplevel domains of advertisers 36 | BlockedApps []string `json:"bapp,omitempty"` // Block list of applications by their platform-specific exchange-independent application identifiers. On Android, these should be bundle or package names (e.g., com.foo.mygame). On iOS, these are numeric IDs. 37 | Source *Source `json:"source,omitempty"` // A Source object that provides data about the inventory source and which entity makes the final decision 38 | Regulations *Regulations `json:"regs,omitempty"` 39 | Ext json.RawMessage `json:"ext,omitempty"` 40 | } 41 | 42 | // Validate the request 43 | func (req *BidRequest) Validate() error { 44 | if req.ID == "" { 45 | return ErrInvalidReqNoID 46 | } else if len(req.Impressions) == 0 { 47 | return ErrInvalidReqNoImps 48 | } else if req.Site != nil && req.App != nil { 49 | return ErrInvalidReqMultiInv 50 | } 51 | 52 | for i := range req.Impressions { 53 | imp := req.Impressions[i] 54 | if err := (&imp).Validate(); err != nil { 55 | return err 56 | } 57 | } 58 | 59 | return nil 60 | } 61 | -------------------------------------------------------------------------------- /bidrequest_test.go: -------------------------------------------------------------------------------- 1 | package openrtb_test 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "testing" 7 | 8 | . "github.com/bsm/openrtb/v3" 9 | ) 10 | 11 | func TestBidRequest(t *testing.T) { 12 | var subject *BidRequest 13 | if err := fixture("breq.banner", &subject); err != nil { 14 | t.Fatalf("expected no error, got %v", err) 15 | } 16 | 17 | privacyPolicy := 1 18 | exp := &BidRequest{ 19 | ID: "1234534625254", 20 | Impressions: []Impression{ 21 | { 22 | ID: "1", 23 | Secure: 1, 24 | Banner: &Banner{Width: 300, Height: 250, Position: AdPositionAboveFold, BlockedAttrs: []CreativeAttribute{CreativeAttributeUserInitiated}}, 25 | }, 26 | }, 27 | Site: &Site{ 28 | Inventory: Inventory{ 29 | ID: "234563", 30 | Name: "Site ABCD", 31 | Domain: "siteabcd.com", 32 | Categories: []ContentCategory{ContentCategoryAutoParts, ContentCategoryAutoRepair}, 33 | Publisher: &Publisher{ID: "pub12345", Name: "Publisher A"}, 34 | PrivacyPolicy: &privacyPolicy, 35 | Content: &Content{ 36 | Keywords: "keyword a,keyword b,keyword c", 37 | }, 38 | }, 39 | Page: "http://siteabcd.com/page.htm", 40 | Referrer: "http://referringsite.com/referringpage.htm", 41 | }, 42 | Device: &Device{ 43 | UA: "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.2.16) Gecko/20110319 Firefox/3.6.16", 44 | IP: "64.124.253.1", 45 | OS: "OS X", 46 | JS: 1, 47 | FlashVersion: "10.1", 48 | }, 49 | User: &User{ 50 | ID: "45asdf987656789adfad4678rew656789", 51 | BuyerUID: "5df678asd8987656asdf78987654", 52 | }, 53 | Test: 1, 54 | AuctionType: 2, 55 | TimeMax: 120, 56 | BlockedAdvDomains: []string{"company1.com", "company2.com"}, 57 | } 58 | if got := subject; !reflect.DeepEqual(exp, got) { 59 | t.Errorf("expected %+v, got %+v", exp, got) 60 | } 61 | } 62 | 63 | func TestBidRequest_complex(t *testing.T) { 64 | for _, kind := range []string{"exp", "video", "native"} { 65 | var subject *BidRequest 66 | if err := fixture("breq."+kind, &subject); err != nil { 67 | t.Fatalf("expected no error, got %v", err) 68 | } 69 | if err := subject.Validate(); err != nil { 70 | t.Fatalf("expected no error, got %v", err) 71 | } 72 | } 73 | } 74 | 75 | func TestBidRequest_Validate(t *testing.T) { 76 | subject := &BidRequest{} 77 | if exp, got := ErrInvalidReqNoID, subject.Validate(); !errors.Is(exp, got) { 78 | t.Fatalf("expected %v, got %v", exp, got) 79 | } 80 | subject = &BidRequest{ID: "RID"} 81 | if exp, got := ErrInvalidReqNoImps, subject.Validate(); !errors.Is(exp, got) { 82 | t.Fatalf("expected %v, got %v", exp, got) 83 | } 84 | subject = &BidRequest{ID: "A", Impressions: []Impression{{ID: "1"}}, Site: &Site{}, App: &App{}} 85 | if exp, got := ErrInvalidReqMultiInv, subject.Validate(); !errors.Is(exp, got) { 86 | t.Fatalf("expected %v, got %v", exp, got) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /bidresponse.go: -------------------------------------------------------------------------------- 1 | package openrtb 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | ) 7 | 8 | // Validation errors 9 | var ( 10 | ErrInvalidRespNoID = errors.New("openrtb: response missing ID") 11 | ErrInvalidRespNoSeatBids = errors.New("openrtb: response missing seatbids") 12 | ) 13 | 14 | // BidResponse is the bid response wrapper object. 15 | // ID and at least one "seatbid” object is required, which contains a bid on at least one impression. 16 | // Other attributes are optional since an exchange may establish default values. 17 | // No-Bids on all impressions should be indicated as a HTTP 204 response. 18 | // For no-bids on specific impressions, the bidder should omit these from the bid response. 19 | type BidResponse struct { 20 | ID string `json:"id"` // Reflection of the bid request ID for logging purposes 21 | SeatBids []SeatBid `json:"seatbid"` // Array of seatbid objects 22 | BidID string `json:"bidid,omitempty"` // Optional response tracking ID for bidders 23 | Currency string `json:"cur,omitempty"` // Bid currency 24 | CustomData string `json:"customdata,omitempty"` // Encoded user features 25 | NBR NBR `json:"nbr,omitempty"` // Reason for not bidding, where 0 = unknown error, 1 = technical error, 2 = invalid request, 3 = known web spider, 4 = suspected Non-Human Traffic, 5 = cloud, data center, or proxy IP, 6 = unsupported device, 7 = blocked publisher or site, 8 = unmatched user 26 | Ext json.RawMessage `json:"ext,omitempty"` // Custom specifications in JSon 27 | } 28 | 29 | // Validate required attributes 30 | func (res *BidResponse) Validate() error { 31 | if res.ID == "" { 32 | return ErrInvalidRespNoID 33 | } else if len(res.SeatBids) == 0 { 34 | return ErrInvalidRespNoSeatBids 35 | } 36 | 37 | for i := range res.SeatBids { 38 | sb := res.SeatBids[i] 39 | if err := (&sb).Validate(); err != nil { 40 | return err 41 | } 42 | } 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /bidresponse_test.go: -------------------------------------------------------------------------------- 1 | package openrtb_test 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "testing" 7 | 8 | . "github.com/bsm/openrtb/v3" 9 | ) 10 | 11 | func TestBidResponse(t *testing.T) { 12 | var subject *BidResponse 13 | if err := fixture("bres.single", &subject); err != nil { 14 | t.Fatalf("expected no error, got %v", err) 15 | } 16 | 17 | exp := &BidResponse{ 18 | ID: "BID-4-ZIMP-4b309eae-504a-4252-a8a8-4c8ceee9791a", 19 | SeatBids: []SeatBid{ 20 | { 21 | Bids: []Bid{ 22 | { 23 | ID: "32a69c6ba388f110487f9d1e63f77b22d86e916b", 24 | ImpID: "32a69c6ba388f110487f9d1e63f77b22d86e916b", 25 | Price: 0.065445, 26 | AdID: "529833ce55314b19e8796116", 27 | NoticeURL: "http://ads.com/win/529833ce55314b19e8796116?won=${auction_price}", 28 | AdMarkup: "", 8 | "adomain": [ 9 | "ads.com" 10 | ], 11 | "iurl": "http://ads.com/112770_1386565997.jpeg", 12 | "cid": "52a5516d29e435137c6f6e74", 13 | "crid": "52a5516d29e435137c6f6e74_1386565997", 14 | "dealid": "example_deal", 15 | "attr": [] 16 | } 17 | -------------------------------------------------------------------------------- /testdata/breq.banner.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "1234534625254", 3 | "at": 2, 4 | "tmax": 120, 5 | "imp": [ 6 | { 7 | "id": "1", 8 | "secure": 1, 9 | "banner": { 10 | "w": 300, 11 | "h": 250, 12 | "pos": 1, 13 | "battr": [ 14 | 13 15 | ] 16 | } 17 | } 18 | ], 19 | "badv": [ 20 | "company1.com", 21 | "company2.com" 22 | ], 23 | "site": { 24 | "id": "234563", 25 | "name": "Site ABCD", 26 | "domain": "siteabcd.com", 27 | "cat": [ 28 | "IAB2-1", 29 | "IAB2-2" 30 | ], 31 | "privacypolicy": 1, 32 | "page": "http://siteabcd.com/page.htm", 33 | "ref": "http://referringsite.com/referringpage.htm", 34 | "publisher": { 35 | "id": "pub12345", 36 | "name": "Publisher A" 37 | }, 38 | "content": { 39 | "keywords": "keyword a,keyword b,keyword c" 40 | } 41 | }, 42 | "device": { 43 | "ip": "64.124.253.1", 44 | "ua": "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.2.16) Gecko/20110319 Firefox/3.6.16", 45 | "os": "OS X", 46 | "flashver": "10.1", 47 | "js": 1 48 | }, 49 | "user": { 50 | "id": "45asdf987656789adfad4678rew656789", 51 | "buyeruid": "5df678asd8987656asdf78987654" 52 | }, 53 | "test": 1 54 | } 55 | -------------------------------------------------------------------------------- /testdata/breq.exp.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "1234567893", 3 | "at": 2, 4 | "tmax": 120, 5 | "imp": [ 6 | { 7 | "id": "1", 8 | "iframebuster": [ 9 | "vendor1.com", 10 | "vendor2.com" 11 | ], 12 | "banner": { 13 | "w": 300, 14 | "h": 250, 15 | "wmin": 300, 16 | "hmin": 250, 17 | "pos": 1, 18 | "battr": [ 19 | 13 20 | ], 21 | "expdir": [ 22 | 2, 23 | 4 24 | ] 25 | } 26 | } 27 | ], 28 | "site": { 29 | "id": "1345135123", 30 | "name": "Site ABCD", 31 | "domain": "siteabcd.com", 32 | "sitecat": [ 33 | "IAB2-1", 34 | "IAB2-2" 35 | ], 36 | "page": "http://siteabcd.com/page.htm", 37 | "ref": "http://referringsite.com/referringpage.htm", 38 | "privacypolicy": 1, 39 | "publisher": { 40 | "id": "pub12345", 41 | "name": "Publisher A" 42 | }, 43 | "content": { 44 | "keyword": [ 45 | "keyword1", 46 | "keyword2", 47 | "keyword3" 48 | ] 49 | } 50 | }, 51 | "device": { 52 | "ip": "64.124.253.1", 53 | "ua": "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.2.16) Gecko/20110319 Firefox/3.6.16", 54 | "os": "OS X", 55 | "flashver": "10.1", 56 | "js": 1 57 | }, 58 | "user": { 59 | "id": "456789876567897654678987656789", 60 | "buyeruid": "545678765467876567898765678987654", 61 | "data": [ 62 | { 63 | "id": "6", 64 | "name": "Data Provider 1", 65 | "segment": [ 66 | { 67 | "id": "12341318394918", 68 | "name": "auto intenders" 69 | }, 70 | { 71 | "id": "1234131839491234", 72 | "name": "auto enthusiasts" 73 | }, 74 | { 75 | "id": "23423424", 76 | "name": "data-provider1-age", 77 | "value": "30-40" 78 | } 79 | ] 80 | } 81 | ] 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /testdata/breq.native.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "80ce30c53c16e6ede735f123ef6e32361bfc7b22", 3 | "at": 1, 4 | "cur": [ 5 | "USD" 6 | ], 7 | "imp": [ 8 | { 9 | "id": "1", 10 | "bidfloor": 0.03, 11 | "native": { 12 | "request": "...Native Spec request as an encoded string...", 13 | "ver": "1.0", 14 | "api": [ 15 | 3 16 | ], 17 | "battr": [ 18 | 13, 19 | 14 20 | ] 21 | } 22 | } 23 | ], 24 | "site": { 25 | "id": "102855", 26 | "cat": [ 27 | "IAB3-1" 28 | ], 29 | "domain": "www.foobar.com", 30 | "page": "http://www.foobar.com/1234.html ", 31 | "publisher": { 32 | "id": "8953", 33 | "name": "foobar.com", 34 | "cat": [ 35 | "IAB3-1" 36 | ], 37 | "domain": "foobar.com" 38 | } 39 | }, 40 | "device": { 41 | "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13 (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2", 42 | "ip": "123.145.167.10" 43 | }, 44 | "user": { 45 | "id": "55816b39711f9b5acf3b90e313ed29e51665623f" 46 | }, 47 | "wseat": [ 48 | "771", 49 | "772" 50 | ], 51 | "bseat": [ 52 | "800", 53 | "773" 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /testdata/breq.video.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "0123456789ABCDEF0123456789ABCDEF", 3 | "at": 2, 4 | "tmax": 120, 5 | "imp": [ 6 | { 7 | "id": "1", 8 | "pmp": { 9 | "private_auction": 1, 10 | "deals": [ 11 | { 12 | "id": "1452f.eadb4.7aaa", 13 | "bidfloor": 5.3, 14 | "at": 1, 15 | "wseats": [], 16 | "ext": { 17 | "priority": 1, 18 | "wadvs": [] 19 | } 20 | } 21 | ] 22 | }, 23 | "video": { 24 | "mimes": [ 25 | "video/x-flv", 26 | "video/mp4", 27 | "application/x-shockwave-flash", 28 | "application/javascript" 29 | ], 30 | "api": [ 31 | 1, 32 | 2 33 | ], 34 | "battr": [ 35 | 13, 36 | 14 37 | ], 38 | "boxingallowed": 1, 39 | "delivery": [ 40 | 2 41 | ], 42 | "h": 480, 43 | "linearity": 1, 44 | "maxbitrate": 1500, 45 | "maxduration": 30, 46 | "minbitrate": 300, 47 | "minduration": 5, 48 | "playbackmethod": [ 49 | 1 50 | ], 51 | "pos": 1, 52 | "protocols": [ 53 | 2, 54 | 3 55 | ], 56 | "sequence": 1, 57 | "startdelay": 0, 58 | "w": 640 59 | } 60 | }, 61 | { 62 | "id": "2", 63 | "pmp": { 64 | "private_auction": 1, 65 | "deals": [ 66 | { 67 | "id": "1452f.eadb4.7aaa", 68 | "bidfloor": 3.5, 69 | "at": 1, 70 | "wseats": [], 71 | "ext": { 72 | "priority": 1, 73 | "wadvs": [] 74 | } 75 | }, 76 | { 77 | "id": "1452f.eadb4.f9bc", 78 | "bidfloor": 2.5, 79 | "at": 1, 80 | "wseats": [ 81 | "45", 82 | "165", 83 | "33" 84 | ], 85 | "ext": { 86 | "priority": 2, 87 | "wadvs": [] 88 | } 89 | } 90 | ] 91 | }, 92 | "video": { 93 | "mimes": [ 94 | "video/x-flv", 95 | "video/mp4", 96 | "application/x-shockwave-flash", 97 | "application/javascript" 98 | ], 99 | "api": [ 100 | 1, 101 | 2 102 | ], 103 | "battr": [ 104 | 13, 105 | 14 106 | ], 107 | "boxingallowed": 1, 108 | "delivery": [ 109 | 2 110 | ], 111 | "h": 480, 112 | "linearity": 1, 113 | "maxbitrate": 1500, 114 | "maxduration": 60, 115 | "minbitrate": 300, 116 | "minduration": 30, 117 | "playbackmethod": [ 118 | 1 119 | ], 120 | "pos": 1, 121 | "protocols": [ 122 | 2, 123 | 3 124 | ], 125 | "sequence": 2, 126 | "startdelay": 300, 127 | "w": 640 128 | } 129 | }, 130 | { 131 | "id": "3", 132 | "bidfloor": 2.00, 133 | "video": { 134 | "mimes": [ 135 | "video/x-flv", 136 | "video/mp4", 137 | "application/x-shockwave-flash", 138 | "application/javascript" 139 | ], 140 | "api": [ 141 | 1, 142 | 2 143 | ], 144 | "battr": [ 145 | 13, 146 | 14 147 | ], 148 | "boxingallowed": 1, 149 | "delivery": [ 150 | 2 151 | ], 152 | "h": 480, 153 | "linearity": 1, 154 | "maxbitrate": 1500, 155 | "maxduration": 60, 156 | "minbitrate": 300, 157 | "minduration": 30, 158 | "playbackmethod": [ 159 | 1 160 | ], 161 | "pos": 1, 162 | "protocols": [ 163 | 2, 164 | 3 165 | ], 166 | "sequence": 3, 167 | "startdelay": -2, 168 | "w": 640 169 | } 170 | } 171 | ], 172 | "site": { 173 | "id": "1345135123", 174 | "name": "Site ABCD", 175 | "domain": "siteabcd.com", 176 | "cat": [ 177 | "IAB2-1", 178 | "IAB2-2" 179 | ], 180 | "page": "http://siteabcd.com/page.htm", 181 | "ref": "http://referringsite.com/referringpage.htm", 182 | "privacypolicy": 1, 183 | "publisher": { 184 | "id": "pub12345", 185 | "name": "Publisher A" 186 | }, 187 | "content": { 188 | "cat": [ 189 | "IAB2-2" 190 | ], 191 | "episode": 23, 192 | "id": "1234567", 193 | "keyword": [ 194 | "keyword a", 195 | "keyword b", 196 | "keyword c" 197 | ], 198 | "season": "2", 199 | "series": "All About Cars", 200 | "title": "Car Show" 201 | } 202 | }, 203 | "device": { 204 | "ip": "64.124.253.1", 205 | "ua": "Mozilla/5.0 (Mac; U; Intel Mac OS X 10.6; en-US; rv:1.9.2.16) Gecko/20140420 Firefox/3.6.16", 206 | "os": "OS X", 207 | "flashversion": "10.1", 208 | "js": 1 209 | }, 210 | "user": { 211 | "uid": "456789876567897654678987656789", 212 | "buyeruid": "545678765467876567898765678987654", 213 | "data": [ 214 | { 215 | "id": "6", 216 | "name": "Data Provider 1", 217 | "segment": [ 218 | { 219 | "id": "12341318394918", 220 | "name": "auto intenders" 221 | }, 222 | { 223 | "id": "1234131839491234", 224 | "name": "auto enthusiasts" 225 | } 226 | ] 227 | } 228 | ] 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /testdata/bres.multi.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "BID-4-ZIMP-4b309eae-504a-4252-a8a8-4c8ceee9791a", 3 | "seatbid": [ 4 | { 5 | "bid": [ 6 | { 7 | "id": "24195efda36066ee21f967bc1de14c82db841f07", 8 | "impid": "24195efda36066ee21f967bc1de14c82db841f07", 9 | "price": 1.028428, 10 | "adid": "52a12b5955314b7194a4c9ff", 11 | "nurl": "http://ads.com/win/52a12b5955314b7194a4c9ff?won=${AUCTION_PRICE}", 12 | "adm": "", 13 | "adomain": [ 14 | "ads.com" 15 | ], 16 | "cid": "52a12b5955314b7194a4c9ff", 17 | "crid": "52a12b5955314b7194a4c9ff_1386294105", 18 | "attr": [], 19 | "dealid": "DX-1985-010A" 20 | } 21 | ], 22 | "seat": "42" 23 | }, 24 | { 25 | "bid": [ 26 | { 27 | "id": "24195efda36066ee21f967bc1de14c82db841f08", 28 | "impid": "24195efda36066ee21f967bc1de14c82db841f08", 29 | "price": 0.04958, 30 | "adid": "527c9fdd55314ba06815f25e", 31 | "nurl": "http://ads.com/win/527c9fdd55314ba06815f25e?won=${AUCTION_PRICE}", 32 | "adm": "", 33 | "adomain": [ 34 | "ads.com" 35 | ], 36 | "cid": "527c9fdd55314ba06815f25e", 37 | "crid": "527c9fdd55314ba06815f25e_1383899102", 38 | "attr": [] 39 | } 40 | ], 41 | "seat": "772" 42 | } 43 | ], 44 | "cur": "USD" 45 | } 46 | -------------------------------------------------------------------------------- /testdata/bres.pmp.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "1234567890", 3 | "bidid": "abc1123", 4 | "cur": "USD", 5 | "seatbid": [ 6 | { 7 | "seat": "512", 8 | "bid": [ 9 | { 10 | "id": "1", 11 | "impid": "102", 12 | "price": 5.00, 13 | "dealid": "ABC-1234-6789", 14 | "nurl": "http: //adserver.com/winnotice?impid=102", 15 | "adomain": [ 16 | "advertiserdomain.com" 17 | ], 18 | "iurl": "http: //adserver.com/pathtosampleimage", 19 | "cid": "campaign111", 20 | "crid": "creative112", 21 | "adid": "314", 22 | "attr": [ 23 | 1, 24 | 2, 25 | 3, 26 | 4 27 | ] 28 | } 29 | ] 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /testdata/bres.single.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "BID-4-ZIMP-4b309eae-504a-4252-a8a8-4c8ceee9791a", 3 | "seatbid": [ 4 | { 5 | "bid": [ 6 | { 7 | "id": "32a69c6ba388f110487f9d1e63f77b22d86e916b", 8 | "impid": "32a69c6ba388f110487f9d1e63f77b22d86e916b", 9 | "price": 0.065445, 10 | "adid": "529833ce55314b19e8796116", 11 | "nurl": "http://ads.com/win/529833ce55314b19e8796116?won=${auction_price}", 12 | "adm": "