A synchronous Python client library for the Geev API (https://prod.geev.fr).
It reproduces exactly what the Geev Android app (v8.6.2) sends on the wire -
headers, HMAC request signing and multipart bodies - so it works against the
live service without scraping the website.
The library is object-oriented: GeevClient is the entry point, and the
network-accessing entities are User and Article. Nothing is fetched at
object construction - every method performs its own HTTP request only when
you call it.
from geev import GeevClient
geev = GeevClient()
geev.login("you@example.com", "s3cret") # returns Session, stored on client
# A User handle (no network call yet) ...
user = geev.get_user("YOUR_USER_ID")
profile = user.profile() # GET /v3/users/{id} (lazy)
page = user.articles(operation="donations") # GET /v3/users/{id}/items
# ... and Articles
article = geev.get_article(page.items[0]["id"])
print(article.title, article.is_reservable)
article.details() # GET /v3/items/{id} (lazy)- Install
- Quick examples
- API reference
- Models
- Errors
- How it matches the app
- Testing
- Disclaimer
- Project layout
uv sync
pip install -e .
# optional, for tests
pip install -e ".[tests]"Requires Python ≥ 3.11 and requests.
from geev import GeevClient
geev = GeevClient() # prod API by default
session = geev.login("jane@example.com", "s3cret")
user = geev.get_user("YOUR_USER_ID")
page = user.articles(operation="donations", limit=10)
print(len(page.items), "articles; next cursor:", page.next_after)
for raw in page.items:
print(raw["id"], raw["title"], raw.get("status"))geev = GeevClient()
geev.login(email, password)
results = geev.search_articles(text="chaise", limit=20) # placement defaults to top_categories
for article in results:
print(article.id, article.title, article.city, article.is_reservable)While the API has a reservation endpoint, ordering another user's item is a destructive side effect on the platform - use with care and only with accounts you control:
session = geev.login(email, password)
article = geev.get_article(ARTICLE_ID)
reservation = article.reserve() # recipient = logged-in user
print(reservation.reservationId)user = geev.get_user("YOUR_USER_ID")
user.profile() # first call does the network round-trip
user.articles() # ...
user.carbon_summary() # ... on demand, not at constructiongeev.GeevClient(base_url=None, language="fr", token=None, session=None)
| Arg | Default | Meaning |
|---|---|---|
base_url |
https://prod.geev.fr/v3 |
also https://dev.geev.fr/v3, https://stage.geev.fr/v3
|
language |
"fr" |
value of the language header on every call |
token |
None |
skip login if you already have an appToken
|
session |
None |
a pre-built Session (userId + token) |
The client stores the current token on .token and the full session on
.session, and passes the token to every authenticated request.
Auth
| Method | Endpoint | Notes |
|---|---|---|
check_email(email) -> bool |
POST /auth/email/check |
True if available |
signup(first_name, last_name, email, password, marketing_consent=False, picture_path=None, referral_code=None) -> Registration |
POST /accounts/local (multipart) |
returns accountId/userId; account not yet active; referral_code (invite code) applied after validation |
resend_validation(account_id) |
POST /accounts/{accountId}/resend-validation |
- |
validate_account(account_id, code) -> Session |
POST /accounts/{accountId}/validate |
activates account, stores token |
login(email, password) -> Session |
POST /auth/local/login |
stores token |
request_password_reset(email) |
POST /accounts/request-password-reset |
emails a reset link |
reset_password(account_id, token, password) |
PATCH /accounts/{accountId}/reset-password |
sets a new password with the emailed reset token |
logout() |
POST /auth/logout |
destructive: invalidates the token |
signup returns a Registration; you then validate with the 6-digit code
emailed by Geev:
reg = geev.signup(first_name="Jane", last_name="Doe",
email="jane@example.com", password="S3cret!")
session = geev.validate_account(reg.accountId, "123456") # code from the emailTo attach an invite / referral (sponsor) code to the new account, pass it to
signup; it is applied automatically once the account is validated
(PUT /users/me/sponsor):
reg = geev.signup(first_name="Jane", last_name="Doe",
email="jane@example.com", password="S3cret!",
referral_code="CODEA1234")
session = geev.validate_account(reg.accountId, "123456")Articles
| Method | Endpoint | Notes |
|---|---|---|
search_articles(text=None, article_type=None, states=None, categories=None, distance=None, latitude=None, longitude=None, placement="top_categories", mode="standard", limit=20, skip=1) -> List[Article] |
POST /search/items |
skip is 1-based (0 is rejected); placement is one of the server's accepted values, see below |
get_article(article_id) -> Article |
GET /items/{articleId} |
wraps the payload |
reserve_article(article_id, recipient_user_id=None) -> Reservation |
POST /reservations |
defaults the recipient to the logged-in user |
cancel_reservation(reservation_id, reason=None) |
DELETE /reservations/{reservationId} |
cancels a reservation (reason, if given, also sends PATCH /reservations/{id}/justify-cancellation); releases the article |
publish_donation(...) -> ArticleCreated |
POST /items/donation (multipart) |
create a "for free" listing |
publish_request(...) -> ArticleCreated |
POST /items/request (multipart) |
create a "looking for" listing |
publish_sale(...) -> ArticleCreated |
POST /items/sale (multipart) |
create a paid listing |
delete_article(article_id) |
DELETE /items/{articleId} |
closes your own listing (no longer orderable) |
Publishing creates a new article owned by the logged-in user; each method
accepts the common fields (title, description, category, latitude,
longitude) plus type-specific ones and an optional pictures list of
image-byte blobs (each sent as a pictures multipart part) and an extra
dict for unwrapped server fields:
created = geev.publish_donation(
title="Table en bois",
description="Table ronde 4 personnes en bon état.",
category="table", latitude=48.8566, longitude=2.3522,
item_state="good",
pictures=[open("table.jpg", "rb").read()],
)
print(created.id, created.validated)placement values accepted by the server: home_listing, top_categories,
home_exclusivities, home_near_you, home_sales,
my_formula_contact_advantages, not_found, explorer,
favorites_carousel. top_categories supports keyword text filters.
Messaging / contact the vendor
| Method | Endpoint | Notes |
|---|---|---|
get_conversation(conversation_id) -> Conversation |
GET /conversations/{conversationId} |
fetch thread + history |
contact_article(article_id, message, dry_run=False, confirm=False) -> Conversation |
POST /items/{articleId}/contact |
starts/reuses the chat with the author |
request_adoption(article_id, message, dry_run=False) -> dict |
POST /adoptions |
{itemIds, message} - expresses intent, does not reserve |
list_conversations(item_id=None, with_archived=False) -> list |
GET /self/conversations |
one article summary per thread |
Inbox, reserved deals, delivery
| Method | Endpoint | Notes |
|---|---|---|
get_inbox(with_archived=False) -> List[ConversationSummary] |
GET /self/conversations |
inbox: one summary per thread, with latest message + unread count |
get_reserved_collections() -> List[ConversationSummary] |
... | inbox entries where a deal is reserved (vendor accepted) |
give_article(article_id, *, recipient_user_id=None, reservation_id=None, communication_grade=None, punctuality_grade=None, feedback=None) -> GeevGiven |
PATCH /reservations/{id}/give |
vendor hands the deal over; reuses an existing reservation or reserves first; donations require communication_grade/punctuality_grade
|
confirm_adoption(reservation_id, *, communication_grade, punctuality_grade, feedback=None) -> AdoptionConfirmed |
PATCH /reservations/{id}/confirm-adoption |
adopter confirms the donation was delivered; closes the deal |
confirm_order(article_id, *, recipient_user_id=None, firstname=None, lastname=None) -> OrderConfirmed |
POST /reservations |
buyer confirms a sale order |
Favorites / wishlist
| Method | Endpoint | Notes |
|---|---|---|
list_favorites() -> List[Favorite] |
GET /v1/api/v0.19/user/self/favorites/ad |
the account's saved favorites (wishlist) |
is_favorite(article_id) -> bool |
GET /v1/api/v0.19/user/self/favorites/ad/{articleId} |
whether the article is favorited |
add_favorite(article_id) |
POST /v1/api/v0.19/user/self/favorites/ad/{articleId} |
save an article to the wishlist; None
|
remove_favorite(article_id) |
DELETE /v1/api/v0.19/user/self/favorites/ad/{articleId} |
drop it from the wishlist; None
|
set_favorite(article_id, favorite: bool) |
... | convenience: adds when True, removes when False
|
The favorites (wishlist) endpoints only exist on the legacy v1 API
surface (/v1/api/v0.19/), so the client targets that base for them. Each
returned Favorite carries the snake_case GeevAd payload in .raw (_id,
title, author._id, location.city, favorite, ...). Article.is_favorite
/ add_favorite / remove_favorite delegate to the matching client method.
Users
| Method | Endpoint | Notes |
|---|---|---|
get_user(user_id) -> User |
– | no network call |
get_me() -> User |
– | a User handle for the logged-in session (session.userId); no network call |
Blocking
| Method | Endpoint | Notes |
|---|---|---|
block_user(user_id) |
POST /v3/users/{userId}/block |
block a user so they can't contact the account; None
|
unblock_user(user_id) |
DELETE /v3/users/{userId}/block |
undo a block; None
|
blocking_status(user_id) -> UserBlockingStatus |
GET /v3/blocking/user/{userId} |
read-only; whether the account blocked the user (have_blocked) or vice-versa (has_been_blocked) |
list_blocked_users(limit=None, after=None) -> BlockedUsers |
POST /v3/blocking |
page of blocked users; .next cursor for the next page |
geev.users.User is created via client.get_user(user_id) and fetches on
demand. All listing/profile calls require the client to be logged in.
| Method | Endpoint | Return |
|---|---|---|
profile() |
GET /v3/users/{userId} |
raw dict (firstName, lastName, firstIntention, _links, ...) |
first_name, last_name (properties) |
– | called profile() lazily |
articles(operation="donations", status=None, after=None, limit=50) -> Page |
GET /v3/users/{userId}/items |
Page{items, next_after, raw} |
iter_articles(operation="donations", status=None, page_size=50) -> Iterator[dict] |
same, cursor-following | yields every item across pages |
reviews(type=None, after=None, limit=20) -> List[Review] |
GET /v3/users/{userId}/reviews |
- |
carbon_summary(temporality=None, light=False) -> CarbonSummary |
GET /v3/users/{id}/carbonSummary |
temporality ∈ ever, thisYear, thisMonth
|
blocking_status() -> UserBlockingStatus |
GET /v3/blocking/user/{userId} |
have_blocked / has_been_blocked flags |
block() |
POST /v3/users/{userId}/block |
block this user; None
|
unblock() |
DELETE /v3/users/{userId}/block |
undo a block; None
|
operation is required by the server: donations or requests. For
donations, pass status=["AVAILABLE"] to see only what can be ordered
today; the app's default is ["AVAILABLE","RESERVED","GIVEN","ACQUIRED"].
The response exposes a cursor in Page.next_after (an article id) for the
next page.
geev.articles.Article wraps a listing/search payload. Convenience read-only
properties (id, title, description, type, state, status,
category, universe, picture, pictures, city, author_id,
author_name, carbon_value, savings, price, stock, validated,
is_reservable) never hit the network - they read the payload that created
the object.
| Method | Endpoint | Return |
|---|---|---|
details() |
GET /v3/items/{articleId} |
raw dict with description, status, creditCost, donator, pictures
|
reserve(recipient_user_id=None) -> Reservation |
POST /v3/reservations |
destructive; defaults to logged-in user |
cancel_reservation(reservation_id=None, reason=None) |
DELETE /v3/reservations/{id} |
destructive; cancels the article's reservation (defaults to the existing one, like give) |
give(recipient_user_id=None, reservation_id=None, communication_grade=None, punctuality_grade=None, feedback=None) -> GeevGiven |
PATCH /v3/reservations/{id}/give |
destructive; vendor hands over the article (reuses an existing reservation or reserves first); donations require the grades |
related() -> List[Article] |
GET /v3/items/{id}/related |
similar articles |
contact(message, dry_run=False, confirm=False) -> Conversation |
POST /v3/items/{id}/contact |
message the vendor; thread is fetched |
request_adoption(message, dry_run=False) -> dict |
POST /v3/adoptions |
{itemIds, message}; intent, no reserve |
delete() |
DELETE /v3/items/{id} |
closes this article (must be yours); no longer orderable |
Example - start a conversation with the vendor of an article:
article = geev.get_article(ARTICLE_ID)
conversation = article.contact("Bonjour, c'est encore disponible ?")
print(conversation.status) # e.g. CONTACTED
send = conversation.send_message("Parfait, merci !")If the account has several conversations without a verified phone number, the
server answers 428 and the payload advertises a confirmContact link -
retry with **contact(..., confirm=True)**.
Contacting (or requesting adoption of) a Geev Plus–gated item, such as a
premium headphone, with an account that has no Geev Plus subscription raises
GeevPlusRequired (HTTP 403 CannotContactItemWithoutGeevPlusError):
geev.conversations.Conversation wraps a messaging thread. Created via
client.get_conversation(id), article.contact(...), or implicitly by
client.contact_article(...); details are fetched once (populating .raw,
.item_id, .status and .messages).
| Field / method | Meaning |
|---|---|
conversation_id |
thread id |
item_id, status, messages
|
fetched fields (after fetch()) |
reservation_id, reservation
|
deal attached to the thread (after fetch()) |
fetch() -> Conversation |
GET /v3/conversations/{id} |
send_message(text) -> Message |
POST /v3/conversations/{id}/message |
list_open(client, item_id=None, with_archived=False) -> list |
GET /v3/self/conversations |
Example - complete a donation deal:
reserved = geev.get_reserved_collections()
deal = next(s for s in reserved if s.given and not s.acquired)
conversation = geev.get_conversation(deal.conversation_id)
geev.confirm_adoption(conversation.reservation_id,
communication_grade=5.0, punctuality_grade=5.0)The sign-up flow mirrors the app:
-
check_email(email)- optional pre-check. -
signup(..., referral_code=None)- multipartPOST /accounts/local, returnsRegistration; an optionalreferral_codeis applied once the account is validated. -
validate_account(account_id, code)-POST /accounts/{accountId}/validate; the response carries theappToken=X-Geev-Tokenused afterwards. If areferral_codewas given tosignup, it is now sent viaPUT /users/me/sponsor. -
login(email, password)-POST /auth/local/login, same token mechanism. -
request_password_reset(email)-POST /accounts/request-password-reset; Geev emails a reset link carrying theaccountId+token. -
reset_password(account_id, token, password)-PATCH /accounts/{accountId}/reset-password; sets a new password with the emailedtoken. -
logout()-POST /auth/logout; invalidates the current token (subsequent requests will 401).
There is no persistence in the library: tokens live only in memory on the
client object. To reuse a session across runs, capture session.appToken and
session.userId yourself and build a new client with
GeevClient(token=..., session=...).
There is no user-lookup-by-name endpoint in the Geev API. Users are identified solely by their
userId(the last path segment of a profile URL likehttps://www.geev.fr/profile/<id>).
| Class | Fields |
|---|---|
Session |
appToken, userId, sso, userType
|
Registration |
accountId, userId
|
Reservation |
reservationId, itemId, raw
|
Page |
items, next_after, raw
|
Review |
id, grade, message, raw
|
CarbonSummary |
year, month, carbonValue, donations, adoptions, equivalences, raw
|
Location |
label, city, postalCode, latitude, longitude, radius, obfuscated
|
Message |
id, author_id, timestamp, text, read_by_receiver, raw
|
Conversation |
handle class; see §3.4 |
ConversationSummary |
inbox entry: id, title, status, reserved/given/acquired/closed, conversation_id, latest_message, unseen_count, raw
|
OrderConfirmed |
reservation_id, conversation_id, raw
|
AdoptionConfirmed |
big_savings, carbon_value, savings, raw
|
ArticleCreated |
id, validated, raw
|
GeevGiven |
give receipt: reservation_id, article_id, raw
|
Favorite |
wishlist entry: id, title, type, category, universe, description, state, is_favorite, author_id, author_name, city, raw
|
UserBlockingStatus |
has_been_blocked, have_blocked, raw
|
BlockedUser |
block entry: id, user_id, first_name, last_name, picture, is_premium, is_investor, raw
|
BlockedUsers |
page: users: List[BlockedUser], next, raw
|
Every model also carries the raw server payload in .raw so you can access
fields the library does not wrap yet.
All exceptions derive from geev.exceptions.GeevError.
| Exception | Raised when |
|---|---|
BadRequest |
HTTP 4xx, or a malformed/unexpected body |
ServerError |
HTTP 5xx |
AuthenticationError |
HTTP 401/403, incl. wrong validation code |
GeevPlusRequired |
the item can only be ordered/contacted with an active Geev Plus subscription |
ValidationError |
client-side argument validation |
BadRequest and its subclasses expose .status_code, .payload, .method
and .url. GeevPlusRequired additionally exposes the account that needs
Plus (.adopter_id) and the affected item (.article_id).
from geev import GeevClient, AuthenticationError, GeevPlusRequired
try:
geev.login("jane@example.com", "wrong-password")
except AuthenticationError as e:
print(e) # includes HTTP status and payload
try:
article.contact("Bonjour !")
except GeevPlusRequired as e:
print("Upgrade to Geev Plus to order", e.article_id)The library reproduces the exact wire behaviour of Geev 8.6.2:
-
Global headers on every request:
User-Agent,x-geev-device-model,geev-app-version,geev-device,timezone, plus per-calllanguage,X-Geev-Token(when logged in),Content-typeandAccept. -
Request signing (
x-geev-timestamp+x-geev-request-signature): HMAC-SHA256 overbody_bytes || timestamp_mswith the key extracted from the app'sSignatureInterceptor. Only present when the request has a body. In this library the body is serialized before signing, so the signed bytes are exactly the bytes on the wire. - Multipart sign-up body is built manually (OkHttp byte-for-byte compatible) so signing stays exact.
Reverse-engineered from the decompiled APK; the endpoint reference doc is
[RAW_API_DOC.md](docs/RAW_API_DOC.md).
The test suite runs against the live production API (prod.geev.fr). It
is marked live; destructive operations (reserve, logout) are not
executed automatically.
pytest tests/test_live.py -m live -vCredentials come from environment variables, or from a .env file at the
project root (copy .env.example and fill it in — the .env file is gitignored):
| Variable | Meaning |
|---|---|
GEEV_SIGNING_KEY |
HMAC key used to sign request bodies |
GEEV_TEST_TOKEN |
appToken of a logged-in account |
GEEV_TEST_USER |
the account's own userId
|
GEEV_TARGET_USER |
another (existing) user who has posted articles |
GEEV_TEST_ARTICLE_ID |
an article id used by the messaging tests |
Real environment variables take precedence over values from .env (so CI
secrets passed by the platform win).
This is NOT the official Geev API.
This is an unofficial, reverse-engineered SDK, not affiliated with or endorsed by Geev. I am in no way responsible for any use made of this SDK. I strongly condemn any abuse of Geev's service made through this library, and I do not guarantee that this library works — it may break at any time and is provided as-is, without warranty of any kind. Respect their ToS, or you are going to be banned.
./
├── docs
│ ├── API.md # doc for the endpoints used in this project
│ └── RAW_API_DOC.md # doc produced by a LLM while reversing the app
├── pyproject.toml
├── README.md # this document
├── geev/
│ ├── __init__.py # public exports
│ ├── _http.py # headers, signing, multipart, transport
│ ├── _settings.py # .env / env-var loading
│ ├── exceptions.py # error types
│ ├── models.py # value objects (Session, Page, ...)
│ ├── auth.py # signup / signin / logout / validate
│ ├── users.py # User class + user operations
│ ├── articles.py # Article class + search / reserve
│ ├── conversations.py # Conversation class + messaging
│ ├── publish.py # publish/creation of articles (multipart)
│ └── client.py # GeevClient facade
└── tests/
├── conftest.py # fixtures (live API credentials)
└── test_live.py # live API tests