geev_unofficial_api
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)
Table of contents
- Install
- Quick examples
- API reference
- Models
- Errors
- How it matches the app
- Testing
- Disclaimer
- Project layout
1. Install
uv sync
pip install -e .
# optional, for tests
pip install -e ".[tests]"
Requires Python ≥ 3.11 and requests.
2. Quick examples
Sign in and explore a user's donations
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"))
Search offers
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)
Reserve / order an article
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)
Lazy user methods
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 construction
3. API reference
3.1 GeevClient
geev.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 email
To 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 |
3.2 User
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.
3.3 Article
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):
3.4 Conversation
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)
3.5 Auth flow - signup, signin, logout
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>).
4. Models
| 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.
5. Errors
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)
6. How it matches the app
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).
7. Testing
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 -v
Credentials 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).
8. Disclaimer
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.
9. Project layout
./
├── 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
Release files for geev-unofficial-api 1.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| geev_unofficial_api-1.3.0.tar.gz | 46.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| geev_unofficial_api-1.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 86.6 kB
Release files / geev_unofficial_api-1.3.0.tar.gz
| Download URL | geev_unofficial_api-1.3.0.tar.gz |
|---|---|
| Size | 46.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c03b5f8ddacedf39ed407fdab9185dff483bb556f5a5dacaf03f3cecb87e1b97
|
|
BLAKE2b-256 checksum How to use checksums |
86ae3f46327d60092d8c91a0b7f12f2b8716c782afacbc2c2f65c96c299993ff
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.8.14
|
Release files / geev_unofficial_api-1.3.0-py3-none-any.whl
| Download URL | geev_unofficial_api-1.3.0-py3-none-any.whl |
|---|---|
| Size | 40.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
48ba4bd24e5e91066fe33dcc3205da1bb14edaebee829d08c188ac89c8fd9ada
|
|
BLAKE2b-256 checksum How to use checksums |
3c313ae3aa73cb310697dd0db902a5a27d4ec377eb5fa79f0d11f72305016463
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.8.14
|