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.
414 lines
12 KiB
Markdown
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.
|