# mrktplaats Go SDK for the [marktplaats.nl](https://www.marktplaats.nl) mobile API. ## Installation ```bash go get github.com/joren/mrktplaats ``` ## Quick start ```go package main import ( "context" "fmt" "log" "github.com/joren/mrktplaats" ) func main() { ctx := context.Background() // Unauthenticated client (search, browse) anon := mrktplaats.NewClient() results, err := anon.Search.Fetch(ctx, &mrktplaats.SearchRequest{ Query: "fiets", Page: 1, Size: 10, ShowListings: true, }, nil) if err != nil { log.Fatal(err) } for _, item := range results.Items { if l := item.Listing(); l != nil { fmt.Printf("%s — %s\n", l.AdCore.URN, l.AdCore.Title) } } // Authenticated client client := mrktplaats.NewClient() login, err := client.Auth.Login(ctx, "email@example.com", "password") if err != nil { log.Fatal(err) } // handle 2FA if needed (login.Verification != nil) _ = login // Or use a saved token directly authed := mrktplaats.NewClient(mrktplaats.WithAccessToken("your-token")) me, err := authed.Users.Me(ctx) fmt.Printf("Logged in as %s\n", me.Name) } ``` ## Client options | Option | Description | |--------|-------------| | `WithAccessToken(token)` | Set a pre-existing access token | | `WithHTTPClient(hc)` | Provide a custom `*http.Client` | | `WithBaseURL(url)` | Override the base URL (default: `https://app.marktplaats.nl`) | | `WithSession(id)` | Set the `x-adevinta-session-id` header | | `WithGAClientID(id)` | Set the `x-adevinta-ga-client-id` header | --- ## Services ### Auth — `client.Auth` #### `Login(ctx, email, password) (*LoginResponse, error)` Authenticates with email + password. If 2FA is required the response has `Verification != nil`. ```go login, err := client.Auth.Login(ctx, "email", "pass") if login.Verification != nil { // prompt for SMS code auth, err := client.Auth.VerifyCode(ctx, login.Verification.RequestID, "123456") client.SetAccessToken(auth.Auth.AccessToken) } ``` #### `VerifyCode(ctx, requestID, code) (*AuthResponse, error)` Completes SMS 2FA. `AuthResponse.Auth.AccessToken` is the bearer token. --- ### Search — `client.Search` / `anon.Search` #### `Fetch(ctx, req, supportedTypes) (*SearchResponse, error)` Full listing search with filters. ```go resp, err := anon.Search.Fetch(ctx, &mrktplaats.SearchRequest{ Query: "laptop", CategoryID: intPtr(377), // optional Page: 1, Size: 20, SortBy: "SORT_INDEX", // or "DATE_DESC", "PRICE_ASC", "PRICE_DESC" AllowCorrection: true, SearchOnTitleAndDescription: true, ShowListings: true, Languages: []string{"nl-BE"}, }, nil) for _, item := range resp.Items { if l := item.Listing(); l != nil { fmt.Printf("%s €%.2f %s\n", l.AdCore.URN, float64(l.AdCore.Price.PriceAmount)/100, l.AdCore.Title) } } ``` `SearchItem.Listing()` returns the listing or `nil` for non-listing items (ads, banners). #### `KeywordSuggestions(ctx, prefix, categoryID) (*KeywordSuggestionsResponse, error)` Auto-complete suggestions for search input. #### `DisplayTargeting(ctx, req) ([]DisplayTargetingResult, error)` Fetch ad targeting slots for the search results page. --- ### Listings — `client.Listings` / `anon.Listings` #### `Get(ctx, urn) (*ListingDetail, error)` Fetch full listing details (title, description, pictures, seller, bids, attributes). ```go detail, err := client.Listings.Get(ctx, "m2372861012") fmt.Printf("Seller: %s (ID %d)\n", detail.SellerInformation.Name, detail.SellerInformation.ID) fmt.Printf("Min bid: €%.2f\n", float64(detail.CurrentMinimumBid)/100) ``` Key fields: - `detail.AdCore.URN` — listing identifier - `detail.AdCore.Title` / `.Description` — listing text - `detail.AdCore.Price.PriceType` — `"FIXED"`, `"MIN_BID"`, `"FAST_BID"`, `"FREE"`, `"ON_REQUEST"` - `detail.AdCore.Price.PriceAmount` — price in euro cents - `detail.AdCore.Pictures` — slice of `Picture` with URLs at various sizes - `detail.SellerInformation.ID` — seller's user ID (needed for `Relevant.Get`) - `detail.Bids` — current bids - `detail.CurrentMinimumBid` — minimum next bid in cents #### `DisplayTargeting(ctx, req) ([]DisplayTargetingResult, error)` Fetch ad targeting slots for the listing detail page. --- ### Discovery — `anon.Discovery` #### `Feeds(ctx, desiredCount) (*FeedsResponse, error)` Returns homepage discovery feed metadata (feed IDs, titles). #### `FeedListings(ctx, feedID, opts) (*FeedListingsResponse, error)` Returns listings for a discovery feed (e.g. `"f8"` = "Voor jou"). ```go resp, err := anon.Discovery.FeedListings(ctx, "f8", &mrktplaats.FeedListingsOptions{ Size: 40, PageLocation: "HOMEPAGE_DISCOVERY", }) ``` --- ### Categories — `anon.Categories` #### `All(ctx) (*CategoriesResponse, error)` Returns the full category tree. #### `Buckets(ctx, categoryID) ([]CategoryBucket, error)` Returns sub-category buckets for a category ID. --- ### Messaging — `client.Messaging` #### `Conversations(ctx, opts) (*ConversationsResponse, error)` Lists all conversations for the authenticated user. ```go resp, err := client.Messaging.Conversations(ctx, &mrktplaats.ConversationsOptions{Limit: 50}) for _, conv := range resp.Conversations { fmt.Printf("[%s] %q with=%s unread=%d\n", conv.ID, conv.Title, conv.OtherParticipant.Name, conv.UnreadMessagesCount) } ``` `conv.ItemID` is the listing URN the conversation is about. `conv.LatestMessage()` returns the most recent `*Message` or nil. #### `Messages(ctx, conversationID, offset, limit) (*MessagesResponse, error)` Fetches messages in a conversation. #### `StartConversation(ctx, itemURN, text) (string, error)` Starts a new conversation about a listing. Returns the conversation ID. #### `SendMessage(ctx, conversationID, text) (string, error)` Sends a message in an existing conversation. Returns the message ID. #### `ServerTime(ctx) (time.Time, error)` / `EmailSubscriptions(ctx) (json.RawMessage, error)` --- ### Favorites — `client.Favorites` #### `List(ctx, limit) (*FavoritesResponse, error)` Lists saved/favourite listings. Each `FavoriteItem` has `ItemID` and `CreationDate`. #### `Add(ctx, urns...) ([]FavoriteItem, error)` Adds one or more listings to favourites. #### `Remove(ctx, urns...) error` Removes one or more listings from favourites. --- ### Users — `client.Users` #### `Me(ctx) (*User, error)` Returns the authenticated user's profile (ID, name, email, postcode). #### `Reviews(ctx, userID, role) (*ReviewsResponse, error)` Returns reviews for a user. ```go resp, err := client.Users.Reviews(ctx, userID, mrktplaats.ReviewRoleReviewee) fmt.Printf("%.1f / 5 (%d reviews)\n", resp.Summary.AverageScore, resp.Summary.NumberOfReviews) ``` `role` is one of `ReviewRoleAll`, `ReviewRoleReviewee`. --- ### MyAccount — `client.MyAccount` #### `MyAds(ctx, opts) (*MyAdsResponse, error)` Lists the authenticated user's own listings. ```go resp, err := client.MyAccount.MyAds(ctx, &mrktplaats.MyAdsOptions{ Status: "active", // "active", "inactive", "sold" Limit: 50, Counts: true, }) ``` #### `DeleteAds(ctx, itemIDs, reason) error` Removes listings. `reason` is one of: - `DeleteReasonSoldOnTweedehands` (1) - `DeleteReasonSoldElsewhere` (2) - `DeleteReasonNotSelling` (3) - `DeleteReasonOther` (4) #### `UpdateProfile(ctx, req) error` Updates name, phone number, postcode, or NDFC declaration status. --- ### SYI (Sell Your Item) — `client.SYI` #### `Form(ctx, categoryID) (*SYIFormResponse, error)` Returns the listing creation form (attributes, supported values) for a category. #### `UploadImage(ctx, img, filename) (string, error)` Uploads an image and returns its `pictureId`. **Large images are automatically compressed** to stay within the API's 900 KB size limit. ```go f, _ := os.Open("photo.jpg") defer f.Close() pictureID, err := client.SYI.UploadImage(ctx, f, "photo.jpg") ``` #### `Create(ctx, req) (*CreateAdResponse, error)` Publishes a new listing. Returns `CreateAdResponse`; use `.URN()` to get the new listing's ID. ```go resp, err := client.SYI.Create(ctx, &mrktplaats.CreateAdRequest{ CategoryID: 1831, // Horloges | Heren Translations: []mrktplaats.Translation{{ Locale: "nl-BE", Title: "Seiko SARV001", Description: "Automatisch horloge in goede staat.", }}, PriceInCents: 25000, // €250 PriceType: "FIXED", PictureIDs: []string{pictureID}, DeliveryMethod: "Ophalen", ShippingConfig: mrktplaats.ShippingConfig{Carriers: []string{}}, SellerName: "Jonathan", Postcode: "8000", SelectedBundle: "FREE", SYISessionID: fmt.Sprintf("session-%d", time.Now().UnixMilli()), IntegratedShippingForTWHEnabled: true, }) fmt.Println(resp.URN()) // "m2373048475" ``` #### `PriceSuggestion(ctx, categoryID, title) (*PriceSuggestionResponse, error)` Returns price range suggestions (low/mid/high) based on similar listings. #### `AttributeSuggestions(ctx, categoryID, text) (json.RawMessage, error)` Suggests attribute values (brand, condition, etc.) from a description. #### `SuspiciousKeywordsWarnings(ctx, categoryID, text) (json.RawMessage, error)` Checks a description for policy-violating keywords. --- ### Saleability — `client.Saleability` #### `Recognize(ctx, img, filename) (*RecognizeResponse, error)` Runs image recognition to predict the category and attributes for a photo. **Large images are automatically compressed.** ```go f, _ := os.Open("photo.jpg") resp, err := client.Saleability.Recognize(ctx, f, "photo.jpg") for _, p := range resp.Predictions { fmt.Printf("%s (%.0f%% confidence)\n", p.CategoryName, p.Confidence*100) } ``` --- ### Enquiry (Bidding) — `client.Enquiry` #### `PlaceBid(ctx, itemURN, req) (*BidsResponse, error)` Places a bid. Value is in **euro cents**. ```go resp, err := client.Enquiry.PlaceBid(ctx, "m2372861012", &mrktplaats.PlaceBidRequest{ Value: 2000, // €20.00 PersonalMessage: "Graag een bod van €20", }) // resp.Bids contains all bids, resp.CurrentMinimumBid is the new minimum ``` #### `RemoveBid(ctx, bidID) (*BidsResponse, error)` Removes a bid by its numeric ID. --- ### Relevant — `anon.Relevant` #### `Get(ctx, listingURN, sellerID, categoryID) (*SimilarAdsResponse, error)` Returns similar/related listings. `sellerID` is required (use `detail.SellerInformation.ID` from `Listings.Get`). --- ### Notifications — `client.Notifications` #### `UnreadCount(ctx) (*UnreadCountResponse, error)` Returns the number of unread notifications. --- ### Payments — `client.Payments` #### `CartItemCount(ctx) (*PaymentCartCountResponse, error)` Returns the number of items in the payment cart. --- ### Config — `client.Config` #### `Labs(ctx) (*LabsConfig, error)` Returns A/B and feature switch configuration. --- ## Error handling ```go detail, err := client.Listings.Get(ctx, urn) if err != nil { if mrktplaats.IsNotFound(err) { // 404 — listing doesn't exist } if mrktplaats.IsUnauthorized(err) { // 401 — token expired or invalid } // generic: err contains status code and response body } ``` `*APIError` exposes `StatusCode int` and `Body []byte` for custom handling. --- ## Image compression Both `SYI.UploadImage` and `Saleability.Recognize` automatically compress images that exceed 900 KB before uploading. The compression: 1. Resizes the image (starting at 1200 px max dimension, stepping down by 25% each iteration) 2. Re-encodes as JPEG at quality 80 3. Stops when the result is under 900 KB Unsupported image formats are passed through unchanged. --- ## Running integration tests ```bash # First run — performs a full login with 2FA prompt go test -v -tags integration -run TestIntegration -timeout 180s # Subsequent runs — reuses the saved token from .test-token go test -v -tags integration -run TestIntegration -timeout 180s ``` Tests cover all 16 service areas against the live API.