Files
Joren 25410749db Initial Marktplaats SDK scaffold
Clone the twdehands SDK into mrktplaats and retarget naming and defaults for app.marktplaats.nl while keeping the same request/response bodies and endpoint behavior.
2026-04-15 23:45:49 +02:00

414 lines
12 KiB
Markdown

# 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.