Bonsanuncios Partner API — OAuth 2.0 integration

Version 1 · Base URL: https://www.bonsanuncios.pt · Ukrainian version: partner_api_oauth_uk.md

Your CRM can publish, update and remove the real-estate listings of any number of agencies on Bonsanuncios. Each agency connects its Bonsanuncios account to your CRM itself, once, with one click (OAuth 2.0): there is nothing to set up on our side per agency.

Agency action in your CRMBonsanuncios endpoint
"Connect Bonsanuncios"GET /oauth_authorize.php → POST /oauth_token.php
PublishPOST /api/v1/listings
UpdatePUT /api/v1/listings/{external_id}
Remove a listingDELETE /api/v1/listings/{external_id}
"Disconnect Bonsanuncios"POST /oauth_revoke.php (or DELETE /api/v1/connection)

What we need from you: your CRM's name, your redirect URI(s) (https). What you get from us: a client_id, a client_secret. Keep the secrets on your servers only.


1. Connecting an agency (OAuth 2.0, authorization code)

1.1 Send the agency to Bonsanuncios

GET https://www.bonsanuncios.pt/oauth_authorize.php
    ?response_type=code
    &client_id=<client_id>
    &redirect_uri=https://<your-crm>/oauth/bazarok/callback   (exactly as registered)
    &state=<random value, checked by your CRM on return>
    [&code_challenge=<BASE64URL(SHA256(verifier))>&code_challenge_method=S256]   (PKCE, optional)

The agency logs in to Bonsanuncios (or creates an account), sees what your CRM will be allowed to do, and clicks Allow or Deny. Bonsanuncios then redirects to the redirect_uri:

  • allowed: ?code=<code>&state=<state> — the code is valid 10 minutes and works once;
  • denied: ?error=access_denied&state=<state>.

1.2 Exchange the code (server to server)

POST https://www.bonsanuncios.pt/oauth_token.php
Authorization: Basic base64(client_id:client_secret)      (or client_id / client_secret in the body)
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=<code>&redirect_uri=<same redirect_uri>[&code_verifier=<verifier>]
{ "access_token": "…", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "…", "account_id": 123456 }

account_id is the agency's Bonsanuncios account id: store it with the agency's tokens. Each agency has its own tokens.

1.3 Refresh

The access token lasts 1 hour, the refresh token 1 year. Refresh tokens rotate: each refresh returns a new pair and invalidates the previous refresh token — always store the new one.

POST /oauth_token.php        grant_type=refresh_token&refresh_token=<refresh_token>     (client authentication as above)

Refresh when you get 401 token_expired, or a few minutes before expires_in.

1.4 Disconnect

POST /oauth_revoke.php       token=<access or refresh token>        (client authentication as above)

Revokes the whole connection between this agency and your CRM (all its tokens). Reconnecting requires the agency to go through 1.1 again. Same effect: DELETE /api/v1/connection with the access token.

OAuth errors follow RFC 6749: {"error": "invalid_grant", "error_description": "…"}.


2. API conventions

  • Every call: Authorization: Bearer <access_token of the agency>; JSON bodies with Content-Type: application/json, UTF-8. Dates: YYYY-MM-DD HH:MM:SS, server time (Europe/Paris).
  • Errors: HTTP status + a stable machine-readable code (test the code, not the message):
{ "error": { "code": "validation_error", "message": "Some fields are missing or invalid.",
             "fields": { "price": "required, number > 0", "contact.phone": "invalid phone number: 12" } } }
HTTPerror.codeMeaning / what to do
401invalid_tokenToken unknown or revoked: ask the agency to connect Bonsanuncios again.
401token_expiredRefresh the token and retry.
422validation_errorMissing/invalid fields — fields lists them (show them next to the fields).
422listing_rejectedThe listing could not be saved (usually the location).
403account_expiredThe agency's trial/subscription has ended: a Bonsanuncios subscription is needed.
403quota_exceededActive listings limit of the agency reached.
409already_existsPOST with an external_id already published: use PUT.
404not_foundUnknown listing.
429rate_limitedMore than 120 requests in a minute for this agency: wait Retry-After seconds.

2.1 Working with hundreds of agencies

  • Throttle your calls: at most 5 requests in parallel for your whole CRM, and one at a time per agency. Publishing downloads the photos during the call (up to ~60 s), so a first import of an agency with hundreds of listings takes time: run it as a background queue.
  • Retry 5xx answers and timeouts later with an increasing delay (1 min, 5 min, 30 min…). Do not retry 4xx answers unchanged.
  • Only send changes: PUT a listing when it changed in your CRM, not on every sync.

Limit: 120 requests per minute per agency (headers X-RateLimit-Limit and X-RateLimit-Remaining on every answer). Above it: 429 rate_limited with a Retry-After header (seconds).

3. Account — GET /api/v1/me

{ "account_id": 123456, "name": "ESTATYQ", "email": "…", "phone": "…",
  "status": "trial",                       // trial | active | expired
  "trial_ends_at": "2026-12-07 10:00:00",
  "subscription": null,                    // or { "status": "active", "paid_until": "…" }
  "listings": { "active": 212, "allowed": 300 },
  "site": { "domain": "bonsanuncios.pt", "country_code": "ua",
            "currencies": ["uah", "usd", "eur"],   // accepted by "currency" (the first one is the default)
            "land_units": ["сотка", "га"] } }      // accepted by "land_area_unit" (the first one is the default)

The API works the same way on every site of the group (bazarok.ua, bazarok.pl, bazarok.fr, bazarok.co.uk, buenanuncios.es, bonsanuncios.pt, skelbus.lt): call it on the site where the agency publishes. site gives the currencies and land-area units that site accepts; listing titles are generated in the site's language.

4. Locations

A listing is located with a Bonsanuncios city_id (recommended), or with city + region names (Ukrainian or Russian, exactly as in Bonsanuncios).

  • GET /api/v1/locations/cities?q=Київ → { "cities": [ { "city_id": 1, "name": "Київ", "name_ru": "Киев", "region": "Київська область", "region_ru": "…" } ] }
  • GET /api/v1/locations/districts?city_id=1 → { "districts": [ { "district_id": 7, "name": "Печерський", "name_ru": "Печерский" } ] }

Map your own cities to city_id once and store the mapping (it is the same for all agencies).

5. Listings

external_id = the property id in your CRM (1–60 characters A-Z a-z 0-9 . _ -), unique per agency. It identifies the listing in all later calls; Bonsanuncios's own id is returned for information.

Publish — POST /api/v1/listings → 201

{
  "external_id": "CRM-48213",
  "deal_type": "rent",                 // sale | rent
  "property_type": "apartment",        // apartment | room | house | land | commercial | garage
  "price": 18000, "currency": "uah",   // one of site.currencies (GET /me) — monthly price for rent
  "description": "Світла 2-кімнатна квартира…",   // 10–10000 characters
  "rooms": 2, "area": 54.5, "living_area": 30,   // m²
  "land_area": null, "land_area_unit": "сотка",   // unit: one of site.land_units (GET /me)
  "floor": 5, "floors_total": 9,
  "lift": true, "balcony": true, "new_building": false, "parking": false, "furnished": true,
  "city_id": 1, "district": "Печерський", "address": "вул. Лесі Українки, 10",
  "lat": 50.4301, "lon": 30.5402,
  "photos": [ "https://cdn.example.com/1.jpg", "https://cdn.example.com/2.jpg" ],   // up to 15, public http(s) URLs, first = cover
  "contact": {
    "name": "John",
    "phone": "0671234567,0501234567"
  }
}

Required: external_id, deal_type, property_type, price, description, city_id (or city + region). The title is generated by Bonsanuncios from the type, rooms, area and location.

contact (optional): the realtor of the agency in charge of this listing.

  • name: up to 100 characters.
  • phone: required inside contact; 1 to 5 numbers, comma-separated ("0671234567,0501234567") or a JSON array (["+380671234567", "0501234567"]); digits, spaces, + ( ) -, 9 to 15 digits each.
  • Visitors who click "Show phone number" on the listing get the first number, with the contact's name; the other numbers are kept and returned by the API.
  • Without contact, the phone of the agency account is shown.

Response:

{ "listing": { "id": 9876543, "external_id": "CRM-48213", "status": "active",
               "url": "https://www.bonsanuncios.pt/oholoshennya/…-9876543.html",
               "title": "Квартира 2-кімн. оренда Київ, Печерський – 54.5 м²", "photos_count": 2,
               "contact": { "name": "John", "phone": "0671234567,0501234567" },
               "updated_at": "2026-10-27 14:02:11" } }

status: active (online) · expired · inactive · deleted.

Update — PUT /api/v1/listings/{external_id} → 200

Same body as publish: full replacement of price, text, photos, characteristics, location and contact (external_id may be omitted). A field you leave out is cleared — in particular, send contact again or the agency's phone will be shown. Photos are downloaded again only if the URL list changed.

Status — GET /api/v1/listings/{external_id} · Remove — DELETE /api/v1/listings/{external_id}

A removed listing can be published again with POST and the same external_id.

6. Getting started

  1. Send us your CRM name, redirect URI(s); we send back client_id and client_secret.
  2. Add a "Connect Bonsanuncios" button for agencies (1.1), exchange the code (1.2), store the tokens with account_id.
  3. Map your cities to city_id (GET /api/v1/locations/cities).
  4. With a test agency: publish a listing, open the returned url, update it, remove it.

Questions: contact@bonsanuncios.pt