先发现词汇,再组合筛选
按 brands → models → search → listing 的顺序调用。用受支持的品牌标签查询车型,将选中的车型带入搜索。filter-options 可辅助了解筛选词表;搜索只接受契约声明的字段,body 内未声明的字段以 INVALID_INPUT(400)拒绝。
在搜索上限内分页
page 保持在 1–750 内,page_size 只选 25、50、100 或 200。每个意图同时保存 Idempotency-Key、动作和带 body 包装的准确 JSON。遇到超时就暂停流程,保留请求标识与原意图,不生成新键,也不再次发起请求。预留保留至释放,绝不迟到扣费;原键只用于核对该意图。同键核对可能返回 REQUEST_PENDING(409),释放后返回 OUTCOME_UNKNOWN_RELEASED(504);超时之后才产生的成功结果不会再返回。成功流程与超时流程必须分开;只有调用者才能明确决定是否建立新意图。
逐条读取广告
先检查搜索行确实含有非空字符串标识,再将其传给 listing。ad_id 是搜索返回的 4–12 位数字标识,不是 4–12 位数字的标识可能失败。只把应用实际使用的车辆字段(如 ad_id、brand、model)写入存储,不复制整条广告。
一个示例中的两种情景
下方 JavaScript 不发起网络请求。情景 A 从 brands 到 listing 的每个原始响应均成功。独立情景 B 在超时后停止,用原身份核对预留,最终观察到释放,不会变成成功,也不会迟到扣费。另有示例覆盖准入错误的顶层 requestId、结果过期及终态失败。示例响应中的 chargedCreditUnits 为 "0",Idempotency-Key 为固定值;真实调用应为每个意图生成新键,成功时按该操作的每次调用价格扣费。
// Example workflow over the five operations; responses are built in memory, with no network calls.
// 示例流程:五个操作的响应在内存中构造,不发起网络请求。
const isRecord = value => value !== null && typeof value === 'object' && !Array.isArray(value);
const isText = value => typeof value === 'string' && value.trim().length > 0;
function requireValue(condition, message) {
if (!condition) throw new Error(message);
}
function intent(action, body, identity) {
// Persist one Idempotency-Key per unchanged intent.
// The fixed keys below belong to this example; generate a new key for each real intent.
return Object.freeze({
method: 'POST', path: `/api/v1/polovniautomobili/${action}`,
headers: Object.freeze({ 'Idempotency-Key': identity }),
json: JSON.stringify({ body }) // Every input sits under body.
});
}
function inspect(response, request) {
requireValue(isRecord(response), 'Missing gateway envelope');
if (response.ok === true) {
requireValue(isRecord(response.meta) && isText(response.meta.requestId) &&
Object.hasOwn(response, 'data'), 'Invalid success envelope');
return { kind: 'success', data: response.data }; // Each step below checks the fields it uses.
}
requireValue(response.ok === false && isRecord(response.error) &&
isText(response.error.code) && isText(response.error.message), 'Invalid failure');
const code = response.error.code;
if (!Object.hasOwn(response, 'meta')) {
requireValue(isText(response.requestId), 'Missing admission request identity');
return { kind: 'admission-error', code, requestId: response.requestId };
}
requireValue(isRecord(response.meta) && isText(response.meta.requestId), 'Invalid error metadata');
const requestId = response.meta.requestId;
if (code === 'REQUEST_PENDING') {
requireValue(['dispatching', 'uncertain'].includes(response.meta.status), 'Invalid pending state');
return { kind: 'pending', code, requestId, checkIntent: request };
}
if (code === 'OUTCOME_UNKNOWN_RELEASED' || response.meta.status === 'released') {
requireValue(response.meta.chargedCreditUnits === '0', 'Unexpected released charge');
return { kind: 'released', code, requestId }; // Stop: new intent requires caller decision.
}
if (code === 'RESULT_EXPIRED') {
return { kind: 'expired', code, requestId }; // No redispatch and no assumed refund.
}
if (response.meta.status === 'uncertain') {
return { kind: 'uncertain', code, requestId, checkIntent: request }; // Stop; never new-key retry.
}
return { kind: 'failed', code, requestId }; // Includes stored terminal errors; no auto-retry.
}
function success(data, requestId) {
return { ok: true, data, meta: { requestId, chargedCreditUnits: '0',
simulated: true, sourceAttribution: {} } };
// chargedCreditUnits is '0' in this example; a real success is charged the per-call price.
}
function requireData(result) {
requireValue(result.kind === 'success' && isRecord(result.data), 'Stop: no usable data');
return result.data;
}
// Scenario A: each original response succeeds; this is NOT timeout recovery.
const brandsIntent = intent('polovniautomobili-brands', {}, 'example-brands-cedar');
const brands = requireData(inspect(success({ brands: [{ label: 'Volkswagen' }] },
'example-request-brands'), brandsIntent));
requireValue(Array.isArray(brands.brands) && brands.brands.every(row =>
isRecord(row) && isText(row.label)), 'Unexpected brands layout');
const brand = brands.brands.find(row => row.label === 'Volkswagen')?.label;
requireValue(brand === 'Volkswagen', 'Choose a supported exact brand enum label');
const modelsIntent = intent('polovniautomobili-models', { brand, sample_pages: 4 },
'example-models-cedar'); // sample_pages: integer 1–10.
const models = requireData(inspect(success({ models: [{ model: 'example-model-cedar' }] },
'example-request-models'), modelsIntent));
requireValue(Array.isArray(models.models) && models.models.length > 0 &&
models.models.every(row => isRecord(row) && isText(row.model)), 'Unexpected models layout');
const searchIntent = intent('polovniautomobili-search', {
brand, model: [models.models[0].model], page: 2, page_size: 100
}, 'example-search-cedar'); // page 1–750; page_size 25/50/100/200.
const search = requireData(inspect(success({ cars: [{ ad_id: '9182736405' }] },
'example-request-search'), searchIntent)); // Original successful response.
requireValue(Array.isArray(search.cars) && search.cars.length > 0 &&
search.cars.every(row => isRecord(row) && isText(row.ad_id)), 'Unexpected search layout');
const listingIntent = intent('polovniautomobili-listing', { ad_id: search.cars[0].ad_id },
'example-listing-cedar'); // ad_id: 4–12 digits; this site accepts any string for ad_id at admission.
const listing = requireData(inspect(success({ car: { ad_id: '9182736405',
brand, model: models.models[0].model, seller: { name: 'example-seller',
phones: ['example-contact'], address: 'example-address',
coordinates: 'example-location' } } }, 'example-request-listing'), listingIntent));
requireValue(isRecord(listing.car) && isText(listing.car.ad_id) &&
listing.car.ad_id === search.cars[0].ad_id && isText(listing.car.brand) &&
isText(listing.car.model), 'Unexpected listing layout');
// Keep only the vehicle fields this application uses.
const stored = [{ ad_id: listing.car.ad_id, brand: listing.car.brand, model: listing.car.model }];
// Scenario B: independent timeout intent; NEVER feeds listing or the success workflow.
const timeoutIntent = intent('polovniautomobili-search', {
brand: 'Volkswagen', page: 3, page_size: 100
}, 'example-timeout-cedar');
const timedOut = inspect({ ok: false,
error: { code: 'OPERATION_TIMEOUT', message: 'The operation did not finish in time and may still have run. Do not automatically repeat this request with a new Idempotency-Key.' },
meta: { requestId: 'example-timeout-request', chargedCreditUnits: '0', status: 'uncertain' }
}, timeoutIntent); // HTTP 504: stop here; no later result will be delivered.
requireValue(timedOut.kind === 'uncertain', 'Expected an uncertain result');
const sameIntentCheck = timedOut.checkIntent; // Preserve key, route and exact JSON.
requireValue(sameIntentCheck === timeoutIntent, 'Do not replace the timeout intent');
const pending = inspect({ ok: false,
error: { code: 'REQUEST_PENDING', message: 'This request was already dispatched. Do not submit a new key to retry an uncertain operation.' },
meta: { requestId: 'example-timeout-request', chargedCreditUnits: '0', status: 'uncertain' }
}, sameIntentCheck); // HTTP 409: replay reports state and never runs the operation again.
requireValue(pending.kind === 'pending', 'Expected a pending state');
const released = inspect({ ok: false,
error: { code: 'OUTCOME_UNKNOWN_RELEASED', message: 'This request ended without a usable result. Its key cannot execute another call.' },
meta: { requestId: 'example-timeout-request', chargedCreditUnits: '0', status: 'released' }
}, sameIntentCheck); // HTTP 504 after release, not a recovered success.
requireValue(released.kind === 'released', 'Expected a released hold');
// No late charge. A new intent is never automatic; it needs a deliberate caller decision.
// Independent error cases: an admission error carries requestId at the top level and no meta.
const admission = inspect({ ok: false,
error: { code: 'INVALID_REQUEST', message: 'The request does not match the documented format.' },
requestId: 'example-admission-request'
}, timeoutIntent); // HTTP 400, no meta.
requireValue(admission.kind === 'admission-error', 'Expected an admission error');
const expired = inspect({ ok: false,
error: { code: 'RESULT_EXPIRED', message: 'The result retention period ended. This request will not be executed again.' },
meta: { requestId: 'example-request-listing', chargedCreditUnits: '0', status: 'succeeded' }
}, listingIntent); // HTTP 410: not run or charged again, and not a refund.
requireValue(expired.kind === 'expired', 'Expected an expired result');
const failed = inspect({ ok: false,
error: { code: 'REQUEST_NOT_FULFILLED', message: 'The operation reported that it did not succeed. Nothing was charged.' },
meta: { requestId: 'example-request-failure', chargedCreditUnits: '0', status: 'failure' }
}, listingIntent); // An alternative outcome for the listing request.
requireValue(failed.kind === 'failed', 'Expected a terminal failure');
console.log({ stored, failure: failed.code }); // Logs the stored vehicle fields and the failure code.
服务端请求模板
以下服务端模板以此 API 的第一个操作为例,演示认证与请求标识。请根据场景选择操作,并在 API 页面查看其当前可用状态与价格。超时后,使用同一个 Idempotency-Key 重发未改变的请求。
网关请求始终使用 POST。
使用 JSON 对象,按文档将参数放入 path、query、headers 和 body,省略未使用的位置。input.headers 是受结构校验的数据,不是浏览器请求头。请勿在此填写凭证。按文档保留 body 的数组、标量或 null;省略与 null 含义不同。
# Server-side only. Set ATLAS_API_KEY in your secret manager.
# Set ATLAS_API_ORIGIN to your configured deployment origin.
# Every gateway operation is called with POST.
# Only operations shown as enabled on the API page can be called.
# Generate once per intent; retain the same value on status checks.
REQUEST_ID="$(uuidgen)"
curl --request POST 'https://apigate.vercel.app/api/v1/polovniautomobili/polovniautomobili-brands' \
--header "Content-Type: application/json" \
--header "x-api-key: $ATLAS_API_KEY" \
--header "Idempotency-Key: $REQUEST_ID" \
--data '{"body":{}}'接入检查清单
- API 密钥保存在服务端密钥管理器中,从不发送到浏览器。
- 每个请求携带自己的 Idempotency-Key,并保留到请求得到最终结果为止。
- 集成读取 ok 与 error.code,并按错误码表处理每个错误码。
- 使用原标识检查结果不确定的 POST,绝不盲目更换标识重试。
- 已在 API 页面查看该操作的当前可用状态与价格。