geev-unofficial-api

Synchronous client library for the Geev peer-to-peer donation/sales app (https://www.geev.fr).


License
MIT
Install
pip install geev-unofficial-api==1.3.0

Documentation

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

  1. Install
  2. Quick examples
  3. API reference
  4. Models
  5. Errors
  6. How it matches the app
  7. Testing
  8. Disclaimer
  9. 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 temporalityever, 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:

  1. check_email(email) - optional pre-check.
  2. signup(..., referral_code=None) - multipart POST /accounts/local, returns Registration; an optional referral_code is applied once the account is validated.
  3. validate_account(account_id, code) - POST /accounts/{accountId}/validate; the response carries the appToken = X-Geev-Token used afterwards. If a referral_code was given to signup, it is now sent via PUT /users/me/sponsor.
  4. login(email, password) - POST /auth/local/login, same token mechanism.
  5. request_password_reset(email) - POST /accounts/request-password-reset; Geev emails a reset link carrying the accountId + token.
  6. reset_password(account_id, token, password) - PATCH /accounts/{accountId}/reset-password; sets a new password with the emailed token.
  7. 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 like https://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-call language, X-Geev-Token (when logged in), Content-type and Accept.
  • Request signing (x-geev-timestamp + x-geev-request-signature): HMAC-SHA256 over body_bytes || timestamp_ms with the key extracted from the app's SignatureInterceptor. 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