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