Avtochast

Avtochast native API v1

Base URL: https://www.avtochast.com/api/mobile/v1. Owned by this website; the Android/iOS app in mobile/avtoapp calls it directly. Gateway does not proxy it or authenticate its accounts. There is no shopping cart or checkout API.

Session and errors

GET bootstrap first, retain Set-Cookie values, and send the returned csrf_token as X-CSRF-TOKEN on every write with the cookies. Send Accept: application/json. Store cookies only in OS secure storage. Login, registration and password changes rotate the session; consume every Set-Cookie and returned CSRF token. These routes use the website's session, CSRF, auth.session, account blocking and site-access rules. They are not Bearer-token or cross-origin browser APIs. No automatic replay of failed mutations is safe.

JSON responses are private/no-store and noindex. Errors use {message}; 422 also has errors: {field: [messages]}. Statuses: 401 expired/blocked/unauthenticated, 403 unauthorized action, 404 missing/private resource, 409 unavailable/stale push configuration or stale/already-answered invitation, 419 stale CSRF, 422 validation or entitlement failure, 429 throttle. The group limit is 120 requests/minute, with stricter route limits below. JSON bodies work normally; upload with multipart POST plus _method=PATCH for edits. Shared support requires the additive conversations migration (nullable listing/seller links and support flag). It uses the same accounts, messages and notification worker.

Routes

Paths below are relative to the base URL. “Account” requires a signed-in, unblocked website user; owner and participant scopes are checked server-side.

Method/path Access Request and response
GET bootstrap Public {csrf_token,user,subscriptions_enabled,subscriptions_url,notifications_enabled,firebase_project_id,push_fingerprint,turnstile_enabled,challenge_url,support_enabled,listing_accounts,listing_account_id}. User is null for guests. Disabled subscriptions always return a null URL.
GET catalog Public {makes:[{id,name,models:[{id,name,years:[]}]}],categories:[{id,name,children:[{id,name}]}],cities:[]}. Model IDs are opaque generation selection strings. Pickers can match a typed four-digit year against years (exact production membership), combined with model text; changing make clears model/year.
GET listings Public Filters q (max100), make, model, year, category, `listing_type=sell
GET listings/{listing} Public or listing manager {data:listing}. Authorized managers may read private listings; others require current availability.
GET challenge?action=... Public Isolated HTML Turnstile challenge. Allowed actions login, register, support_contact, support_problem, support_listing. Token sent to WebView JavaScript channel Avtochast.postMessage(token). Native screens remain native.
GET docs Public This full API contract as HTML.
POST register Public, 5/min name max100, unique email max255, password, matching password_confirmation, `account_type=personal
POST login Public, 6/min email,password,remember:boolean; optional challenge field below. Existing password-enabled accounts only. Returns bootstrap (200). Social-only accounts can use password recovery; native OAuth is not included.
POST forgot-password Public, 3/min email; returns neutral {message} whether the account exists. Reset occurs through emailed website link.
POST logout Account {logged_out:true,csrf_token}; invalidates session and detaches its push token.
PATCH profile Account name,account_type, optional phone max30, city from catalog, public_phone:boolean, company fields as above. Publishing phone requires 7–15 valid digits. {user}. Email is not editable here.
PUT password Account, 6/min current_password,password,password_confirmation; same new-password rules. Existing website recent-social-auth rules remain. {message,csrf_token}. Other sessions are invalidated.
GET my-listings Account Selected account listings that the actor may manage, including drafts/sold/moderated; optional listing_account_id pins account context. Same q,make,model,year,category,listing_type,sort,page filters and validation as public listings, plus optional `status=active
GET listings/{listing}/phone Account, 20/min Visible listing and seller's explicit public-phone setting required. {phone} or 404.
POST listings Account, 30/min Listing fields below; {message,id} (200).
PATCH listings/{listing} Listing manager, 30/min Same full listing fields; {message,id}.
GET recent-vehicles Account {data:[{make_id,car_model_id,year,label}]}; at most four distinct donor model/generation/year combinations, newest listing ID first. Uses the same recent-vehicle candidates as the website (latest 50 distinct single-year combinations); excludes moderated/deleted listings and invalid catalog years, includes sold/draft listings. Empty accounts return data:[]. Selected-account scoped, restricted to manageable listings; accepts listing_account_id, private/no-store. car_model_id is the catalog selection ID, including generation.
DELETE listings/{listing}/archive Listing manager Removes from own/public search via soft deletion, marking sold first and preserving listing/image/message history. {message}. No hard delete. Guests receive 401, unauthorized accounts (including unrelated admins) 403; already archived/missing records 404. Distinct from the existing sold action below.
PATCH listings/{listing}/publication Listing manager `status=active
DELETE listings/{listing} Listing manager Marks sold/finished, preserves history. {message}.
GET team Account {accounts,active_account_id,members,invitations}; own invited members and received pending invitations. Shapes and permission rules below.
POST team/invitations Account, 10/min email valid, max255; required manage_all:boolean. Invites to actor's own account; own email or already-active member is 422. Reinviting replaces expired/declined/revoked/pending tokens and sends a new invitation message. {message,invitation_url,email_sent} (200). The recipient sees a card in Team and Messages without email delivery. Email is optional; failed delivery still returns a copyable link.
POST team/accept Account, 10/min invitation string max2048: full invitation URL or id.token. Matching signed-in email AND secret required (403 otherwise); expired 422, revoked/blocked owner 403, unknown ID404. Legacy email-link fallback, idempotent after acceptance; selects inviter's account. {message,invitation}. Declined invitations409.
POST team/invitations/{membership}/{decision} Invited account, 10/min `decision=accept
POST team/select Account Positive listing_account_id: actor's account or accepted active membership only (403 otherwise). Saves session context; {account_id,message}. Refresh bootstrap afterwards.
PATCH team/members/{membership} Inviter only Required manage_all:boolean; {message}. Revoked membership409. Permission applies immediately to open editors and draft access.
DELETE team/members/{membership} Inviter or that accepted member Revokes access/leaves team; {message}. Listings and logs remain with owner. Repeated revocation is idempotent.
GET listing-activity Account owner Only signed-in actor's owned listings/team, irrespective of selected account. Period filters below, optional action, actor positive ID, page1–100000. 30/page; {period,filters,data:[audit],next_page}.
GET listing-statistics Account owner Same owner-only scope and period filters; {owner_name,period,totals,metrics,series,top_listings,tracking_note}. Historical daily views start at feature deployment.
GET conversations Account page/inbox_page (1–100000), inbox_q max120, `inbox_filter=all
POST conversations/support Account, 15/min No fields. Opens/reuses the account’s shared support conversation; first open adds one help request. {conversation_id,redirect} (200). Repeated calls reuse history without another message. Session/CSRF required; no Turnstile required for this authenticated chat.
GET conversations/{conversation}/messages Visible participant Optional before positive message ID; latest30 ascending within page, `{data:[message],before:int
POST listings/{listing}/messages Account, 15/min body 2–4000; available other seller's listing required. Creates/reuses conversation and adds initial message; {conversation_id} (201). Initial send has no idempotency key.
POST conversations/{conversation}/messages Open participant, 20/min body 1–4000, photos[] up to4, optional location_url max2048 validated Google Maps URL, location_label max120, client_id UUID. At least text/photo/location required. {messages:[message],client_id:new UUID}. Reusing sender's client_id returns original message; retain it after uncertain network failures.
POST conversations/{conversation}/updates Visible participant after integer >=0. {messages:[message],cursor,has_more,read_through,closed,restricted}; up to50 newer messages, marks returned incoming messages read; shared chat-sync limiter.
DELETE conversations/{conversation} Participant confirmed:true; closes a listing conversation and hides for caller, preserves other participant's history. {message}. Support conversations stay open; 422 when can_close=false (support and team threads).
GET conversations/{conversation}/images/{message}/{position} Visible participant Position0–3; authenticated private image bytes. Send session cookie; no public media URL.
POST support Account name max100, email max255, subject max160, message 10–8000, cf-turnstile-response; company_website honeypot must be empty. Shared support rate/abuse limits. Saves encrypted contact request in existing admin inbox; {reference,message} (201). No email delivery guarantee.
POST push Account, 20/min token 20–4096 [A-Za-z0-9_:.\-], current 64-character fingerprint from bootstrap. Saved Firebase integration must be enabled/configured. {enabled:true}. Impersonation forbidden. Token encrypted; binding and auth/config fingerprints checked on delivery.
POST push/test Account, 3/min No fields. Requires a device registered in this session and enabled saved Firebase settings. Queues a notification only for that device; {message} confirms queueing, not delivery. 409 if registration/configuration is missing; impersonation forbidden.
DELETE push Account Detaches current session's device token; {enabled:false}. Impersonation forbidden.

Turnstile login/register verification follows the saved enable switch. Support always requires valid server-verified cf-turnstile-response with action support_contact and an allowed website hostname. support_enabled reports whether both saved/fallback keys exist. Challenge tokens are single-use; fetch a new challenge after rejection. Never bypass verification for native clients.

Data shapes and listing validation

user: id,name,email,phone,city,public_phone,account_type,company_name, company_number,display_name,listing_limit. Only the signed-in account receives its private email/phone and effective limit. Public seller shape is {id,name,account_type}.

listing: id,slug,title,description,listing_type,price_cents,currency (EUR), city,part_number,category_id,category,make_id,vehicle,car_model_id,car_models[], year,year_to,images[],status,available,status_label,is_owner,can_manage,listing_account_id,seller,url,created_at. Images are absolute public listing photo URLs. image_paths[] is present only for authorized listing managers, to identify removals; timestamps use ISO8601. Never derive publication permission from cached status; mutations recheck current account/entitlements.

Create accepts optional listing_account_id (positive owner ID); pin this in open editors. Edits always retain the existing owner and require current permission. Create/edit requires title 5–160, category_id, make_id, car_model_id from catalog, year in that model/generation's production years, price as decimal EUR string (1–7 integer digits, optional comma/dot and1–2 decimals), city from catalog, and status=active|draft|sold. Optional listing_type=sell|buy, description max10000, part_number max100, car_models[] 1–30 distinct compatible selection strings belonging to the make. Omitted compatibility preserves existing compatible selections for that make. Photos: photos[] JPEG/PNG/WebP, max25MB each, max80MB per request, max50 megapixels and12000px per edge, max8 retained+new; remove_images[] contains own existing paths (max255). Images are decoded and processed through the shared website flow. Free/paid entitlement, moderation and campaign rules use the listing owner. Invitations never copy, transfer or multiply either user’s allowance.

conversation: id,kind,can_close,person,listing_id,title,preview,unread,updated_at,closed. kind=listing|support|team; support has listing_id=null, can_close=false and title=Помощ с обявите. Customers see “Екипът на Авточаст”; admins see the customer. Only the requesting customer and currently active admins can list, read, reply, poll or fetch support attachments. Admin status does not grant access to other people’s ordinary listing conversations. All admins share the support inbox: one admin reading a customer message marks it read for the team. Admin replies remain unread until the customer reads them; colleagues do not mark them read. Customer messages notify all active admins; replies notify the customer. Existing private push delivery checks apply. Support remains usable without listing quota. Old Turnstile-protected POST support remains the separate support-request form.

message: id,body,sender_id,sender_name,mine,client_id,created_at,read_at,location_url, location_label,images[],team_invitation. Message photo limits match listing upload size limits, with max4 images/message. Nullable values remain null; native clients render plain text. Polling is active only while the relevant screen/app is visible.

Shared listing teams and reports

listing_accounts: [{id,name,personal,manage_all}]; listing_account_id is the selected owner ID (null for guests, accounts empty). Selection defaults to self. listing_account_id in query/body, or X-Listing-Account, overrides session context on create, my-listings and recent-vehicles. Explicit unauthorized selection is403; revoked stale session selection falls back to self. Public catalog/search and messages remain independent of this selection. There is no team access to another account's payments, personal profile, private messages, invitations or reports.

A member without manage_all can create team listings and manage only listings whose created_by_id is that member. With permission, all inviter listings are manageable, including older listings. Owner retains full access. New team listings show the inviter's seller profile and consume the inviter's free allowance/packages; personal listings stay separate. One account can join multiple teams. Existing listing ownership is unchanged; legacy creator attribution stays null. Website new-form drafts are separated by actor and owner, and recheck permissions on access. Publish remains explicit; drafts do not consume a listing entitlement.

team.members: [{id,email,name,manage_all,status,expires_at,invitation_url}] for the signed-in inviter only. Status is active|pending|expired|revoked|declined. Invitation URLs are available only for pending, unexpired invitations. Tokens expire after7 days, are encrypted at rest and are replaced on reinvite. team.invitations contains invitation cards for the signed-in recipient, without the acceptance secret. A card is {id,owner_name,manage_all,version,status,label, expires_at,can_respond,accept_url,decline_url,team_url}. version is a fingerprint of the current invitation, not an authorization token; the server additionally checks recipient identity and binds message conversations to their original buyer. The two action URLs are website POST routes with CSRF; native clients use the API route above. Only pending, unexpired invitations from active owners can be answered.

On invitation, an existing recipient receives an unread message from the inviter in a private kind=team conversation (listing_id=null, can_close=false, title “Покана за екип”). Both participants can discuss the invitation. Other users and unrelated admins cannot access it. The invitation bubble has team_invitation containing the card above; ordinary messages have null. Cards in older messages may also have status=superseded|unavailable. Old message revisions never authorize a newer invitation. Acceptance/decline adds one reply to notify the inviter and acknowledges invitation messages as read; repeating the action adds no duplicate.

Pending invitations predating this feature, or sent before registration, are delivered once when that recipient opens bootstrap, Team, Messages or unread counts. Retries and page refreshes do not duplicate the message. Resending rotates the version and adds one new invitation message in the same conversation. The existing message push queue notifies configured devices, with the normal privacy and delivery checks. Email remains an optional parallel delivery path; log/array mailers or mail failure do not prevent in-app acceptance. Email links still open Team for review and an explicit action, rather than accepting automatically.

Report query: period=week|month|custom (default month), optional anchor=YYYY-MM-DD. Custom requires from and to, inclusive dates; start from2000 through today, end>=start, maximum366 days. Weeks begin Monday in Europe/Sofia. Boundaries are converted to UTC for stored events, including DST. period response: {unit,anchor,from,to,previous,next,can_next}; previous/next are calendar anchors. Use a new anchor to move week/month; custom ranges use explicit from/to.

audit: {id,actor,actor_id,listing_id,title,action,label,source,changes,created_at}. actor_id/listing_id can be null; source=website|mobile; ISO8601 UTC timestamps. changes:[{field,label,before,after}] records content/status/activation changes, image counts (including replacement), compatible vehicles and team permissions. Action filters: created,updated,published,unpublished,sold,deleted,restored, member_invited,member_joined,member_declined,permissions_changed,member_removed. Server-generated logs survive listing archival and membership revocation. They begin at deployment; failed transactions produce no listing history. Compatibility changes can be a separate edit event within the same save.

Statistics totals: {listings,views,active,drafts,sold,deleted} (all-time listing creation/view counts include soft-deleted listings; status counts are current). metrics: {views,created,published,sold,deleted,edited} for the selected period; published/sold/deleted/edited count recorded events, not distinct listings. series:[{day,views,created}] includes every day, including zeros. top_listings:[{id,title,views,total_views}] returns up to10 owned listings ordered by period views then lifetime views. Lifetime views include earlier aggregate data; we cannot reconstruct their historical daily breakdown. New listing dates use existing creation timestamps. New web/native detail views share 24-hour session deduplication; bots, admins, impersonation and people managing the listing are excluded.

Settings, payment and push

Every bootstrap resolves the saved database integration settings; environment values are fallback only. Refresh on app resume and immediately before subscription navigation. When disabled or refresh fails, hide subscription navigation. When enabled, open subscriptions_url in the system browser, where the user signs in and pays using the existing website. The app neither embeds checkout nor handles card data. Store distribution/payment policy review is still a release step.

Native Firebase Android/iOS apps must belong to the same firebase_project_id as the website settings. FCM uses the existing database notifications queue and worker. Server service-account credentials stay on the website. Android notification and APNs alert/sound payloads accompany the existing web data payload; no private message text is placed on the lock screen. The native app asks permission on an explicit profile action, registers the FCM token, refreshes it, and removes it on logout. Notification data.url opens an authorized native conversation. APNs key, iOS capabilities, native app registration and device delivery verification are required before distribution; repository setup alone does not enable delivery.

Example

curl -c cookies.txt -H 'Accept: application/json' \
  https://www.avtochast.com/api/mobile/v1/bootstrap
# Copy csrf_token, then keep cookie rotations with both -b and -c.
curl -b cookies.txt -c cookies.txt -H 'Accept: application/json' \
  -H 'X-CSRF-TOKEN: <csrf_token>' -H 'Content-Type: application/json' \
  -d '{"email":"[email protected]","password":"<password>","remember":true,"cf-turnstile-response":"<fresh challenge token>"}' \
  https://www.avtochast.com/api/mobile/v1/login
curl -b cookies.txt -H 'Accept: application/json' \
  'https://www.avtochast.com/api/mobile/v1/listings?listing_type=sell&sort=newest'

Use a test environment/account for write examples; never commit cookie files, passwords or Firebase server credentials. Deploy the website API before distributing the app. Gateway needs only matching boundary documentation, with no new proxy, permission, environment variable, provisioning, backup or service dependency.