Avigram API

Документация для разработчиков

Введение

Avigram API предоставляет уведомления в реальном времени при обнаружении новых объявлений на Avito. Система отправляет HTTP POST запросы на указанный endpoint со структурированными данными объявлений, позволяя интегрировать с вашими рабочими процессами и базами данных.

Доступ к API и тарифные планы

Обратите внимание: отправка данных через API доступна только на тарифах «Бизнес» и «Индивидуальный».

На тарифах «Базовый» и «Профессиональный» уведомления о новых объявлениях доставляются только в мессенджеры.

Архитектура

Схема работы

Новое объявление
Валидация
HTTP POST
Ваш Endpoint

Цикл запроса

  1. Обнаружение: Новое объявление найдено системой Avigram
  2. Валидация: Объявление проходит внутренние проверки
  3. Трансформация: Сырые данные Avito → Структурированный JSON
  4. Отправка: HTTP POST с логикой повторных попыток
  5. Подтверждение: Ваш сервер отвечает статусом 2xx

Спецификация API

Требования к Endpoint

  • Метод: POST
  • Content-Type: application/json
  • Таймаут: 10 секунд

Заголовки запроса

Content-Type: application/json
User-Agent: Avigram/1.0
X-Avigram-Timestamp: 1710000000000          # только если задан секретный ключ
X-Avigram-Signature: sha256=<hex>           # только если задан секретный ключ
Заголовок Обязательный Описание
Content-Type Да Всегда application/json
User-Agent Да Всегда Avigram/1.0
X-Avigram-Timestamp Нет Unix timestamp в миллисекундах. Присутствует только при заданном секретном ключе
X-Avigram-Signature Нет HMAC-подпись тела. Присутствует только при заданном секретном ключе. Формат: sha256=<hex>

Безопасность (секретный ключ)

В настройках ссылки можно указать секретный ключ (callback_secret). Это необязательно.

Коротко: секретный ключ — опциональная защита. Без него интеграция работает как раньше. С ключом вы получаете HMAC-подпись тела запроса.

Как считается подпись

signature = HMAC-SHA256(secret, timestamp + "." + rawBody)
# заголовок: X-Avigram-Signature: sha256=<hex-digest>

Схема данных

Основной объект

interface AvitoItem {
  id: number;
  title: string;
  price: string | null;
  address: string | null;
  description: string | null;
  time: string;
  timestamp: number;
  link: string;
  markView: string | null;
  delivery: string | null;
  parameters: Parameter[];
  seller: SellerInfo;
  images: string[];
  info: SearchInfo;
}

Параметр объявления

interface Parameter {
  title: string;
  description: string;
}

Объект поиска

interface SearchInfo {
  searchId: number;
  searchName: string;
  searchUrl: string;
  userId: number;
  sentAt: number;
  subExpiresAt: number | null;
}

Объект продавца

interface SellerInfo {
  id: string | null;
  name: string;
  type: "Частное лицо" | "Компания";
  rating: number | null;
  reviews: string | null;
  summary: string | null;
  registration: string | null;
}

Справочник полей

Поле Тип Nullable Описание
id number Нет Уникальный ID объявления Avito
title string Нет Заголовок объявления
price string Да Цена с валютой
address string Да Полный адрес (город + улица)
description string Да Описание объявления (может быть пустым)
time string Нет Время публикации в человекочитаемом формате по МСК
timestamp number Нет Unix timestamp в миллисекундах
link string Нет Прямая ссылка на объявление Avito
markView string Да Маркетинговый бейдж (Рыночная цена и т.д) или null
delivery string Да Информация о доставке или null
parameters Parameter[] Нет Характеристики объявления (состояние, бренд, память и т.д.); пустой массив, если параметров нет
parameters[].title string Нет Название параметра (например: «Состояние», «Бренд»)
parameters[].description string Нет Значение параметра (например: «Б/у», «Apple»)
seller.id string Да Внутренний ID продавца
seller.name string Нет Имя продавца
seller.type string Нет Тип продавца
seller.rating number Да Рейтинг продавца (0.0-5.0)
seller.reviews string Да Текст с количеством отзывов
seller.summary string Да Дополнительная информация о продавце
seller.registration string Да Дата регистрации
images string[] Нет Массив URL изображений (максимум 5)
info.searchId number Нет ID поиска/ссылки в Avigram
info.searchName string Нет Название поиска (в Avigram), по которому найдено объявление
info.searchUrl string Нет URL поиска (на Авито), по которому найдено объявление
info.userId number Нет ID пользователя в Avigram
info.sentAt number Нет Unix timestamp в миллисекундах — момент отправки callback
info.subExpiresAt number | null Да Unix timestamp в миллисекундах — дата истечения подписки пользователя

Обработка ошибок и стратегия повторов

Конфигурация повторов

  • Максимум попыток: 4 (1 основная + 3 повтора)
  • Стратегия задержки: Фиксированные задержки [1000мс, 5000мс, 15000мс]
  • Условия повтора:
    • Проблемы с сетевым подключением
    • Таймаут запроса (>10с)
    • HTTP 429 (Слишком много запросов)
    • HTTP 5xx (Ошибки сервера)
  • Без повтора: HTTP 4xx (ошибки клиента, кроме 429)
⚠️ Если после всех попыток повтора объявление не будет успешно обработано вашим сервером, оно будет считаться доставленным и удалено из очереди. Повторная отправка этого объявления не произойдет. Убедитесь, что ваш endpoint надежно обрабатывает запросы и корректно отвечает статусом 2xx.

Примеры реализации

Ниже — два сценария: без проверки (секрет не задан) и с проверкой HMAC (секрет задан в настройках ссылки).

Node.js (Express) — без проверки подписи

const express = require('express');
const app = express();

app.use(express.json());

app.post('/avigram-callback', async (req, res) => {
  try {
    const item = req.body;

    if (!item.id || !item.title) {
      return res.status(400).json({ error: 'Invalid payload' });
    }

    await processListing(item);
    res.status(200).send('OK');
  } catch (error) {
    console.error('Ошибка обработки avigram callback:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

async function processListing(item) {
  console.log(`Новое объявление: ${item.title} - ${item.price}`);
  console.log(`Поиск #${item.info.searchId} пользователя #${item.info.userId}`);
  await saveListingToDatabase(item);
}

Node.js (Express) — с проверкой HMAC

const express = require('express');
const crypto = require('crypto');
const app = express();

// Важно: для подписи нужен сырой body
app.use(express.json({
  verify: (req, res, buf) => {
    req.rawBody = buf.toString('utf8');
  }
}));

const SECRET = process.env.AVIGRAM_CALLBACK_SECRET; // тот же ключ, что в настройках ссылки
const MAX_SKEW_MS = 5 * 60 * 1000; // 5 минут

function verifyAvigramSignature(req) {
  const timestamp = req.headers['x-avigram-timestamp'];
  const signature = req.headers['x-avigram-signature'];

  if (!timestamp || !signature) {
    return false;
  }

  const ts = Number(timestamp);
  if (!Number.isFinite(ts) || Math.abs(Date.now() - ts) > MAX_SKEW_MS) {
    return false;
  }

  const expected = 'sha256=' + crypto
    .createHmac('sha256', SECRET)
    .update(`${timestamp}.${req.rawBody}`)
    .digest('hex');

  try {
    return crypto.timingSafeEqual(
      Buffer.from(signature),
      Buffer.from(expected)
    );
  } catch {
    return false;
  }
}

app.post('/avigram-callback', async (req, res) => {
  if (!verifyAvigramSignature(req)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  try {
    const item = req.body;
    if (!item.id || !item.title) {
      return res.status(400).json({ error: 'Invalid payload' });
    }
    await processListing(item);
    res.status(200).send('OK');
  } catch (error) {
    console.error('Ошибка обработки avigram callback:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

async function processListing(item) {
  // ваша логика
}

Python (FastAPI) — без проверки подписи

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List, Optional

app = FastAPI()

class Parameter(BaseModel):
    title: str
    description: str

class SellerInfo(BaseModel):
    id: Optional[str]
    name: str
    type: str
    rating: Optional[float]
    reviews: Optional[str]
    summary: Optional[str]
    registration: Optional[str]

class SearchInfo(BaseModel):
    searchId: int
    searchName: str
    searchUrl: str
    userId: int
    sentAt: int
    subExpiresAt: Optional[int]

class AvitoItem(BaseModel):
    id: int
    title: str
    price: Optional[str]
    address: Optional[str]
    description: Optional[str]
    time: str
    timestamp: int
    link: str
    markView: Optional[str]
    delivery: Optional[str]
    parameters: List[Parameter]
    seller: SellerInfo
    images: List[str]
    info: SearchInfo

@app.post("/avigram-callback")
async def receive_callback(item: AvitoItem):
    try:
        await process_listing(item)
        return {"status": "success"}
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

async def process_listing(item: AvitoItem):
    # Ваша реализация здесь
    pass

Python (FastAPI) — с проверкой HMAC

import hmac
import hashlib
import os
import time
from fastapi import FastAPI, Request, HTTPException

app = FastAPI()
SECRET = os.environ["AVIGRAM_CALLBACK_SECRET"]
MAX_SKEW_MS = 5 * 60 * 1000

def verify_signature(raw_body: bytes, timestamp: str | None, signature: str | None) -> bool:
    if not timestamp or not signature:
        return False
    try:
        ts = int(timestamp)
    except ValueError:
        return False
    if abs(int(time.time() * 1000) - ts) > MAX_SKEW_MS:
        return False

    expected = "sha256=" + hmac.new(
        SECRET.encode("utf-8"),
        f"{timestamp}.".encode("utf-8") + raw_body,
        hashlib.sha256,
    ).hexdigest()

    return hmac.compare_digest(signature, expected)

@app.post("/avigram-callback")
async def receive_callback(request: Request):
    raw_body = await request.body()
    ok = verify_signature(
        raw_body,
        request.headers.get("x-avigram-timestamp"),
        request.headers.get("x-avigram-signature"),
    )
    if not ok:
        raise HTTPException(status_code=401, detail="Invalid signature")

    item = await request.json()
    # обработка item ...
    return {"status": "success"}

PHP (Laravel) — без проверки подписи

use Illuminate\Http\Request;

Route::post('/avigram-callback', function (Request $request) {
    try {
        $data = $request->validate([
            'id' => 'required|integer',
            'title' => 'required|string',
            'price' => 'nullable|string',
            'address' => 'nullable|string',
            'info' => 'required|array',
            'info.searchId' => 'required|integer',
            'info.searchName' => 'required|string',
            'info.searchUrl' => 'required|string',
            'info.userId' => 'required|integer',
            'info.sentAt' => 'required|integer',
        ]);

        processAvitoListing($data);
        return response()->json(['status' => 'success'], 200);
    } catch (\Exception $e) {
        \Log::error('Ошибка avigram callback: ' . $e->getMessage());
        return response()->json(['error' => 'Processing failed'], 500);
    }
});

PHP (Laravel) — с проверкой HMAC

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;

Route::post('/avigram-callback', function (Request $request) {
    $secret = env('AVIGRAM_CALLBACK_SECRET');
    $timestamp = $request->header('X-Avigram-Timestamp');
    $signature = $request->header('X-Avigram-Signature');
    $rawBody = $request->getContent();

    if (!$timestamp || !$signature) {
        return response()->json(['error' => 'Missing signature'], 401);
    }

    if (abs((int) (microtime(true) * 1000) - (int) $timestamp) > 5 * 60 * 1000) {
        return response()->json(['error' => 'Stale request'], 401);
    }

    $expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);

    if (!hash_equals($expected, $signature)) {
        return response()->json(['error' => 'Invalid signature'], 401);
    }

    try {
        $data = $request->all();
        processAvitoListing($data);
        return response()->json(['status' => 'success'], 200);
    } catch (\Exception $e) {
        Log::error('Ошибка avigram callback: ' . $e->getMessage());
        return response()->json(['error' => 'Processing failed'], 500);
    }
});