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

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

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
│   ├── _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.2.0

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.2.0
File Size Uploaded
geev_unofficial_api-1.2.0.tar.gz 41.2 kB Details

Built distribution (wheel)

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

Total release size: 78.1 kB

Release files / geev_unofficial_api-1.2.0.tar.gz

Download URL geev_unofficial_api-1.2.0.tar.gz
Size 41.2 kB
Tags Source
SHA-256 checksum
How to use checksums
e568fa4bd8c950999c3a5b7f28e26fb8f6f89ea326417e2cf02c402dcd3e1379
BLAKE2b-256 checksum
How to use checksums
fc3199e5aeb4168a50010391551d2974d4b7a6287b61c0af7ac49a2369ef3ec2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.8.14

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

Download URL geev_unofficial_api-1.2.0-py3-none-any.whl
Size 36.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b0dd29fb6e5796dae81e8a25fd63839b7d750b06704a675fb5fd6ff44fbec2c0
BLAKE2b-256 checksum
How to use checksums
ec39b2c21285d0e2c09af6519fe99a67f3cfdd69fa3f37a6759bedac64244681
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

This release

1.2.0 This release

2 release files

1.1.1

2 release files

1.1.0

2 release files

1.0.1

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