# BuyPost Customer API — guide for developers and AI agents Machine-readable spec (OpenAPI 3.1): https://ai.buypost.co.il/customer-api-openapi.json Human docs (Hebrew): https://ai.buypost.co.il/ApiDocs ## What the API does - READ: price list (with the customer's own prices), credits balance, orders, articles and their stages, and what was bought but not used yet (`list_redeemable`). - SUBMIT: new orders of sponsored articles on news/content websites. - ACT where the site allows it: approve an article, request a fix, cancel, change details, redeem a bought-and-paid article (same rules as the site; otherwise 409 with an explanation). - It does NOT pay or buy. Orders are created UNPAID; the customer completes the purchase on https://ai.buypost.co.il/UnpaidOrders (credits or card). Payment moves the articles into the regular BuyPost workflow. ## Connecting - One endpoint: `POST https://ai.buypost.co.il/functions/customerApi` - Header: `Authorization: Bearer bp_live_...` (do NOT use X-Api-Key — it is not passed through). Keep the key on a server; never ship it to a browser or mobile app. - Body: JSON with `action`, e.g. `{"action":"balance"}`. - Response: `{"ok":true,"data":{...}}` or `{"ok":false,"error":{"code","message","hint","details"}}`. Use `code` in code, show `message`/`hint` to people (Hebrew). - Money: ILS. `*_before_vat` = before 18% VAT (credits are before VAT); `*_incl_vat` = card amount. - Keys: an approved customer creates them on https://ai.buypost.co.il/ApiAccess (access is approved by BuyPost on request). ## Getting access, and connecting an AI tool Human-readable page for people and AI tools: https://ai.buypost.co.il/ApiAiGuide 1. The customer requests access on https://ai.buypost.co.il/ApiAccess — an email confirms the request. 2. BuyPost approves (usually within a business day) — an email says access is approved and what to do next. 3. The customer creates a key on the same page. The key (bp_live_...) is both the username and the password; it is shown once and never emailed. Store it on the server as BUYPOST_API_KEY. Up to 5 active keys; revoking one takes effect at once. 4. Optional: set a webhook URL on the key — a signing secret is shown once (store it as BUYPOST_WEBHOOK_SECRET). Tools: - Claude Code / Cursor / Copilot / Gemini: put BUYPOST_API_KEY in .env (git-ignored), paste the prompt from the ApiAccess page, and let the tool read this guide and the OpenAPI spec. Never paste the key itself into the chat. - ChatGPT: (a) ask it to write the integration code; or (b) a custom GPT with an Action — import the spec from https://ai.buypost.co.il/customer-api-openapi.json, Authentication: API key, Bearer. Use a dedicated key you can revoke. - n8n / Make / Zapier: an HTTP Request step (POST to the endpoint, header Authorization: Bearer kept as a credential, JSON body with "action"), plus a Webhook trigger whose URL is set on the key for live updates. ## Actions | action | input | output | |---|---|---| | ping | — | `{pong, account}` | | balance | — | `{credits_balance_before_vat, paid_by, limits, redeemable:{prepaid_articles, orders_waiting_for_payment, waiting_total_before_vat, covered_by_credits}, articles_waiting_for_your_approval, recommendation}` | | list_redeemable (alias redeemable) | — | `{credits:{balance_before_vat, paid_by, recent_activity}, prepaid_articles:{redeemable_count, by_website, items, awaiting_payment, how_to_redeem}, waiting_for_payment:{orders, total_before_vat, covered_by_credits, pay_url}, waiting_for_your_approval:{count, items}, recommendation, next_steps}` | | price_list (alias websites) | `search?, category?, region?, city?, language?, min_dr?, tier?, feature?, on_sale?, instant_publishing?, max_price?, sort? (price_asc / price_desc / dr_desc), limit<=500, offset` | `{total, general_restrictions, websites:[Website]}` — everything the site catalogue shows: `id,name,url,logo_url,description,public_notes,category,language,region,city,dr,ur,turnaround_days,tier_badge,features[],instant_publishing,example_articles[{title,url}],content_tags[],recommended_topics[],writing_notes,publishing_restrictions,single_link_only,website_type,requires_shared_fields,requires_image_approval,image_approval_text,requires_publisher_draft,pixel_option,pixel_price_before_vat,list_price_before_vat,on_sale,sale_price_before_vat,price_before_vat (YOUR price),price_source,fixed_discount_percent`. `general_restrictions` = text the site shows under every website. | | quote | `items[]` | `{subtotal_before_vat, discount_before_vat, total_before_vat, total_incl_vat, lines[{website_id, website_name, list_price_before_vat, price_before_vat, charged_price_before_vat, price_source, coupon_discount, website_rules}], enough_credits}` — nothing is created | | create_order | `idempotency_key, items[1..20], notes?` | `{order, items, payment:{status:'awaiting_payment', total_before_vat, total_incl_vat, pay_at}, replayed, message}` | | get_order | `order_id` | `{order, payment, items}` — every article has `payment_status` and `progress` (also in create_order) | | list_orders | `payment_status? (paid / awaiting_payment / cancelled), source? (site / api), created_from?, created_to? (YYYY-MM-DD), search? (order number), updated_since?, limit<=100, offset` | `{total, checked_at, orders:[{id, order_number, status, payment, coupons, created_at, updated_at, source, articles_count, published_count, waiting_for_you_count, stages{stage:count}, articles[{id, website_name, status, stage, label, waiting_on, waiting_for_you, published_url}]}]}` — every past and current order, site and API | | list_items | `status?, order_id?, created_from?, created_to?, waiting_for_you?, awaiting_redemption?, updated_since?, limit<=200, offset` | `{total, checked_at, items:[Article + payment_status + awaiting_redemption]}` — pass `checked_at` back as `updated_since` to get only changes | | get_items | `item_ids[1..50]` | `{total, checked_at, items}` — several articles in one request | | get_item | `item_id` | `{item}` (+ `content_html` once the article reached the customer) | | approve | `item_id` (status review) | `{item_id, status, message}` | | request_fix | `item_id, note` (status review) | `{item_id, status:'admin_review', message}` — the team fixes it | | submit_edit | `item_id, content_html, note?, keep_for_review?` (status review) | your corrected text, saved with a matching Word file (the website always gets your version). Default: goes to the team's check. `keep_for_review: true` keeps it with you (like the site's "save"); your later `approve` sends it to the team's check | | ai_fix | `item_id, instructions` (status review) | the AI rewrites the article by your instructions (structure, style, links and keywords kept) and returns `content_html`; the article STAYS with you to approve / ai_fix again / submit_edit / request_fix. Up to 5 per article and 20 per 24 hours (`ai_fixes_left_for_article`, `ai_fixes_left_today`); a failed AI run changes nothing (`ai_fix_failed`) | | replace_file | `item_id, filename, file_base64, note?` (status review) | your own Word file replaces the article | | reject_content | `item_id, reason` (status review) | the team writes it again; the new text returns for approval | | cancel | `item_id, reason?` | `{item_id, status:'rejected', refund_before_vat, message}` — allowed before writing starts, after a team fix request, or anytime while the order is unpaid; else 409 `not_cancellable` | | update_item (alias redeem) | `item_id, changes{...}` | `{item_id, status, redeemed?, message}` — unpaid API order: any brief field; `scheduled_date` (null = ASAP) before sending to the website; brief fields when the topic was sent back; **redeem** a bought-and-paid article (`awaiting_redemption`): send article_topic, keywords, target_link, writing_style, optional scheduled_date. Unpaid/cancelled order → 409 `order_not_paid`; a date alone on an unredeemed article → 409 `redeem_details_required`; else 409 `not_editable` | Site-only actions (return 403 `site_only`): pay, buy_credits, set_webhook, remove_webhook, test_webhook, delete, delete_order. ## Order item fields (every option of the site's order form) - `website_id` (from price_list) — required - `content`: `{"type":"ai"}` (BuyPost writes; needs `article_topic` of 3+ words and `writing_style`), `{"type":"html","html":"..."}` (ready article, 300+ text characters, no script/iframe/on* attributes), or `{"type":"docx","filename":"a.docx","file_base64":"..."}` (<= 8 MB) - `writing_style`: professional | marketing | seo_no_brand | seo_with_brand (needs `brand_name`) | ai_engines. SEO-only websites accept only seo_no_brand. - `keywords` (ONE keyword, no commas) + `target_link` (URL) — required - `keywords_2` + `target_link_2` — optional, only together, not on `single_link_only` websites - `scheduled_date` YYYY-MM-DD, not in the past, Sunday–Thursday (Friday/Saturday refused like the site). A dated article stays regular and waits for the team's topic approval. - `image`: `{option:"auto_select"}` (default), `{option:"stock",url}` (a link to an image), or `{option:"upload",images:[{url|file_base64+filename, credit}]}` — 1–2 images, a photo credit for EACH is required. - `shared_with` ("בשיתוף עם") — required where the website has `requires_shared_fields`; `shared_company_id` optional. - `pixel` `{url}` or `{file_base64, filename}` — only where `pixel_option` is free/paid; a paid pixel adds `pixel_price_before_vat`. - `client_label` (your end client), `notes` (for the writer), `team_notes` (for the BuyPost team only). - `redeem_later: true` (+ `quantity` 1–20): buy now, send details later with `redeem` — no details now. - For a ready article (html/docx) `keywords` + `target_link` are optional but always together. - Banned topics: gambling, tobacco/smoking, sexual content, escort services. - Website rules: `price_list` and `quote` lines return each website's `writing_notes`, `publishing_restrictions`, `single_link_only`, `seo_only`, `requires_shared_fields`, `requires_image_approval`, pixel option. The AI writer follows writing_notes and publishing_restrictions. - No link: a ready article (html / docx / your own edit) without any link gets a NON-blocking warning — `warnings: [{index, code: 'no_external_link', message}]` in quote / create_order, and `warnings` on the article (any web address — the publishing website too — or a mailto: / tel: link counts; until you add a link or confirm on the site that it is on purpose). The team sees the same warning before sending to the website. - Errors: `validation_failed` → `error.message` lists the problems per article; `details[].errors[]` = `{code, field, message}` — mark `field` in your form. - Prices: sale > special price > list price, then the customer's fixed discount (and a paid pixel) — each line says `price_source` and `fixed_discount_percent`. - Coupons: the customer's personal coupons (and general coupons assigned to them) apply AUTOMATICALLY, by the same rules as the site's cart — an item coupon (special price / free website) on that website's first article, plus one order-wide coupon (percentage / amount; with a fixed customer discount only a one-time amount). Send `coupon_code` to use a specific code, or `apply_coupons: false` to apply none. `quote` / `create_order` return `coupons[]` and `coupon_discount_before_vat`; each line has `coupon_discount`. A coupon is consumed only when the order is paid; if it was used elsewhere or expired before payment, the order is repriced (credits payment answers 409 `coupon_changed` with the new amount). Bundles stay site-only. ## Article statuses (lifecycle) pending (team approves the topic) → topic_approved/writing → admin_review (team check) → review (waits for the CUSTOMER's approval on the site) → approved → awaiting_image_approval? → sent_to_publisher → publisher_draft_review/draft_review (site draft — approved on the site) → published (`published_url`). `rejected` = stopped (refund per terms). `payment_status`: awaiting_payment → paid. Unpaid orders are never written, sent to a website or published. `awaiting_redemption: true` = bought and paid, waiting for the customer's details (redeem it). `waiting_for_you: true` = the customer has to act (`progress.waiting_on` is customer): approve the text, a website draft (approved on the site), an image approval, a topic the team sent back, missing details. A bought article waiting for its details has `awaiting_redemption` instead. Every article has `progress` = `{stage, label (Hebrew), waiting_on: customer|team|website|payment|null, next_step}` and `available_actions` (the API actions that fit its stage right now); a published article has `published_url` and `published_at` (also in the `article.published` webhook). ## Limits (anti-abuse) — design your client around them - 10 requests per minute and 300 requests per day per ACCOUNT (all its keys together, revoked ones included; BuyPost can change it per customer). Every request counts, including ping and refused ones. `meta.requests_left_today` / header `X-RateLimit-Remaining-Day` show what is left. - Articles per day per account (default 30) and open unpaid API orders (default 5). - 20 wrong-key attempts from one address in 10 minutes → blocked for 10 minutes. - Cache `price_list` on your side (it changes rarely; the server caches it for 5 minutes anyway). - Do not poll. Receive webhooks; when you must check, use `list_items` with `updated_since` (only changes) or `get_items` (up to 50 articles in one request) — never one `get_item` per article. - Read answers may come from a short cache (`meta.cached`, `meta.fresh_for_seconds`). Your own writes, article status changes and credit purchases refresh it immediately. - On 429 wait and retry with backoff. On 409 `busy_try_again` retry after a few seconds. On 5xx retry `create_order` with the SAME `idempotency_key` — it never creates a duplicate. ## Webhooks Configured by the customer on https://ai.buypost.co.il/ApiAccess (a signing secret is shown once). POST JSON `{id, type, created_at, data}`; types: article.waiting_for_approval, article.published, article.rejected, article.status_changed (data: `{item, previous_status}`), order.paid (data: `{order_id, order_number, payment_method, articles_activated}`), credits.added (data: `{credits_added_before_vat, balance_before_vat}` — call `list_redeemable` next), ping. Verify: `X-BuyPost-Signature: sha256=`, reject timestamps older than 5 minutes, answer 2xx within 5 s. ## Recipe: a control panel for this API 1. Settings screen: store the key server-side; call `ping` to verify, `balance` to show credits, limits and what can be redeemed. 1b. "Available to redeem" screen: `list_redeemable` — credits, bought-and-paid articles by website (a Redeem form per article → `redeem`), orders waiting for payment, and `recommendation.message` with a link to `recommendation.buy_credits_url` when credits do not cover the waiting orders. Refresh it on the `credits.added` and `order.paid` webhooks. 2. Catalogue screen: `price_list` (paginate with limit/offset, cache for hours), filters by category/price, show `price_before_vat` and `on_sale`. 3. Order form: build `items`, call `quote` to show the total and validation errors (`details[].errors[].message` per item index), then `create_order` with a unique `idempotency_key` (e.g. your internal order id). 4. After create_order: show `message` and a button to `payment.pay_at` (the customer pays on BuyPost). 5. Tracking board: `list_items` grouped by `payment_status` and `status`; per article show `progress.label`, `stages`, `published_url`. Show exactly the buttons in the article's `available_actions` — in `review` that is approve / request_fix / submit_edit / ai_fix / replace_file / reject_content (read the text with `get_item` → content_html first). `waiting_for_you` is also true for a website draft (draft_review / publisher_draft_review): that one is approved on the BuyPost site, and `available_actions` is then empty. Handle 409 `not_cancellable` / `not_editable` by showing `error.message`. 6. Live updates: receive webhooks, verify the signature, update the article by `data.item.id`; on `order.paid` mark the order paid; on `credits.added` refresh balance / list_redeemable. 7. Errors: map `error.code` to UI states; show `error.message` + `error.hint`. ## Minimal examples ```bash curl -X POST https://ai.buypost.co.il/functions/customerApi \ -H "Authorization: Bearer bp_live_YOUR_KEY" -H "Content-Type: application/json" \ -d '{"action":"price_list","limit":20}' ``` ```js // Node 18+ async function buypost(action, body = {}) { const res = await fetch('https://ai.buypost.co.il/functions/customerApi', { method: 'POST', headers: { Authorization: `Bearer ${process.env.BUYPOST_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ action, ...body }), }); const json = await res.json(); if (!json.ok) throw Object.assign(new Error(json.error.message), json.error); return json.data; } ```