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
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 |
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 |
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 |
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 |
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 |
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 |
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.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 |
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. 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.1.1
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.1.1.tar.gz | 39.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| geev_unofficial_api-1.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 75.1 kB
Release files / geev_unofficial_api-1.1.1.tar.gz
| Download URL | geev_unofficial_api-1.1.1.tar.gz |
|---|---|
| Size | 39.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d25186ef17f7b4f178d2066681f10d283e4856c0377c80800da6147beb097512
|
|
BLAKE2b-256 checksum How to use checksums |
7713efb3ca8c1f4c5d8aaa6a21bcc98e5981611094e6821f3537ce9efc11d0be
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.8.14
|
Release files / geev_unofficial_api-1.1.1-py3-none-any.whl
| Download URL | geev_unofficial_api-1.1.1-py3-none-any.whl |
|---|---|
| Size | 35.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
18c7b6f3b1e2a7d078e50cdd38205d7fe9a5c2a72c3108ca65749fa3ea6fe20c
|
|
BLAKE2b-256 checksum How to use checksums |
975406649c74deac19fd5aa3a5b4ac20981124e9f103115e4388b482b330381b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.8.14
|