Skip to main content

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. 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
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

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)
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 temporalityever, 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
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

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. 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

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. 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
│   ├── 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
│   └── client.py              # GeevClient facade
└── tests/
    ├── conftest.py            # fixtures (live API credentials)
    └── test_live.py           # live API tests

Release files for geev-unofficial-api 1.0.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for geev-unofficial-api 1.0.1
File Size Uploaded
geev_unofficial_api-1.0.1.tar.gz 33.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for geev-unofficial-api 1.0.1
File Interpreter ABI Platform
geev_unofficial_api-1.0.1-py3-none-any.whl Python 3 none any Details

Total release size: 64.3 kB

Release files / geev_unofficial_api-1.0.1.tar.gz

Download URL geev_unofficial_api-1.0.1.tar.gz
Size 33.9 kB
Tags Source
SHA-256 checksum
How to use checksums
9cd8618ffce9f83cf434e8583cc5c0fd95bf0979a8bfeb0d9390d0c62f9decc6
BLAKE2b-256 checksum
How to use checksums
924ebefa7771a59ccc794918571b74c6ec771ebfdc1e4df60adf5af4da20cfd2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.8.14

Release files / geev_unofficial_api-1.0.1-py3-none-any.whl

Download URL geev_unofficial_api-1.0.1-py3-none-any.whl
Size 30.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f48a25e0e3022199b2822b2a20ba744b8aec3502fbd5d610aaf5404bec722156
BLAKE2b-256 checksum
How to use checksums
3d618fc54faa96d5dc854613846eb8f1fcbce311c74620923508d460a686ff4d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.8.14

Release history Release notifications | RSS feed

1.3.0

2 release files

1.2.0

2 release files

1.1.1

2 release files

1.1.0

2 release files

This release

1.0.1 This release

2 release files

1.0.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page