
Heyşirket Sync API v1
The Heyşirket Sync API lets your online store, marketplace software, ERP or accounting system automatically send customers, products (with colour/size variants and images), stock, sales invoices, expenses/incoming e-invoices, cash and bank movements and e-commerce orders to your company on Heyşirket, and read back the changes made in Heyşirket (stock, prices, new variants, payments, order actions and more).
- Base URL:
https://portal.heysirket.com/api/v1 - Machine-readable definition:
/api/v1/openapi.json· limits and constants:/api/v1/meta - Raw version of this page (for AI tools):
/en/developers/sync-api.md· Turkish original:/gelistiriciler/senkron-api
Integrating with AI (ready-made prompt)
Copy the text below as it is and give it to your AI coding assistant (Claude, ChatGPT, Cursor and so on). Fill in the parts in square brackets with your own details. Do not put your API key in the prompt; ask the assistant to read the key from an environment variable (HEYSIRKET_API_KEY).
You are a senior integration engineer. Your task: build an uninterrupted, two-way and secure sync between
[SYSTEM NAME — e.g. my WooCommerce store / my own e-commerce software / my ERP] and Heyşirket.
Sources (read all of them first, make no assumptions):
- Guide: https://heysirket.com/en/developers/sync-api.md
- OpenAPI: https://portal.heysirket.com/api/v1/openapi.json
- Limits/constants: https://portal.heysirket.com/api/v1/meta
Context:
- The API key for my Heyşirket company is in the HEYSIRKET_API_KEY environment variable. Never log the key or
write it into code, the repository or error messages.
- My source system: [language/framework, database, hosting — e.g. PHP 8 + WordPress/WooCommerce, MySQL, cPanel cron].
- The permanent code (source) for my system: [e.g. mystore] — lowercase letters, digits, hyphens.
- My sales channels: [e.g. my own site (woocommerce), trendyol, hepsiburada].
- My e-invoice provider: [e.g. Trendyol E-Faturam / none].
What to do (in order, testing each step):
1. Build a data mapping: map every entity in my system (customer, supplier, product + variant + image, stock,
order, invoice, return/cancellation, expense, incoming e-invoice, payment/collection) to the fields in the
guide. Use permanent, unchanging IDs as externalId. Send amounts as decimals in the main currency unit and
VAT as a percentage. For products with variants, send only the sellable variants as products[] and the parent
product with family; put properties such as colour/size into attributes with human-readable names. Send
images as https URLs.
2. Set up change tracking: for every entity, store a "last changed time/version" cursor and a fingerprint of the
record you sent; send only what changed. If nothing changed, do not send a request.
3. Set up sending: send in pages with POST /sync (≤400 records per request, invoices+expenses+orders ≤150, body
≤2 MB). Order: business → customers → products(+family) → stock → orders → invoices → expenses →
transactions → payouts → deleted (suppliers with suppliers[], before customers or in the same page).
First send the same data with ?dryRun=1 and fix the mapping until errors is empty.
4. Error handling: errors[] in a 200 response is per record; fix that record and send it again (resending the
same page is safe, it is idempotent). On 429 wait for Retry-After (DAILY_QUOTA: until the next UTC day, queue
the remaining records). On 5xx and network errors use exponential backoff; on 401/402/423 stop and alert the
administrator.
5. Set up the return flow: in every run, FIRST read the changes in Heyşirket with GET /changes?after=<last seq>
and apply them to your system (apply stock quantity as a DIFFERENCE: IN +, OUT −, ADJUSTMENT signed), then
send your own changes. Persist the last applied seq. For a product/variant/family newly created in Heyşirket,
create your own record and match it with heysirketId in the next send.
6. Scheduling: set up a single, locked job that runs every 15 minutes for both the change feed and sending.
Make the first full transfer startable by hand and able to resume where it left off; if it exceeds the daily
quota it should continue the next day.
7. Observability: show the last run time, the number of sent/failed records and the latest errors on your
system's admin screen. Compare with the result on the Heyşirket side using GET /sync/runs.
8. Testing: write unit tests for the mapping functions (amount rounding, line totals equal to the invoice total,
variant/family mapping, deleted records, applying feed events). Verify end to end first with ?dryRun=1, then
with one small real page; check the key with GET /ping.
Rules: Be careful with cents in money calculations; never count the same money twice (a payment comes in either
as an invoice payment or as a cash movement). Do not delete invoices; report a cancellation with status:
"CANCELLED". Send personal data only in the fields listed in the guide. When you finish, write the mapping you
built and the run instructions as a short README.Quick start
- Create a key. Heyşirket → Settings → API and sync → Create key. The key (
hsk_live_…) is shown only once; keep it somewhere safe. Create a separate key for each system. - Test the connection.
curl -s https://portal.heysirket.com/api/v1/ping -H "Authorization: Bearer $HEYSIRKET_API_KEY" - Validate first, then write. Send the same body with
?dryRun=1; iferrorsis empty, remove the parameter.curl -s -X POST "https://portal.heysirket.com/api/v1/sync?dryRun=1" \ -H "Authorization: Bearer $HEYSIRKET_API_KEY" -H "Content-Type: application/json" \ -d '{"source":"mystore","customers":[{"externalId":"c-1","title":"Ayşe Yılmaz"}]}' - See the result. In Heyşirket, the Settings → API and sync screen shows the counts and per-record errors of every transfer.
Authentication and limits
Authorization: Bearer hsk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json| Endpoint | Key | Description |
|---|---|---|
GET /api/v1/meta | not required | limits, constant values, version |
GET /api/v1/openapi.json | not required | OpenAPI 3.1 definition |
GET /api/v1/ping | required | the company the key belongs to |
POST /api/v1/sync | required | bulk, idempotent transfer (?dryRun=1 only validates) |
GET /api/v1/changes?after=0&limit=200 | required | feed of changes made in Heyşirket |
GET /api/v1/sync/runs?limit=20 | required | recent transfers |
| Limit | Value |
|---|---|
| Records per request | at most 400 (invoices + expenses + orders together at most 150) |
| Body size | at most 2 MB |
| Transfer requests | 30 per minute per key (429 RATE_LIMITED, Retry-After: 60) |
| Daily record quota | 5,000 records per company per UTC day (429 DAILY_QUOTA; a large first transfer is spread over the following days) |
| Change feed retention | 30 days (read it at least once a day) |
General rules
- Idempotent upsert: a record that arrives with the same
source+externalIdis updated, not duplicated. Resending the same page after a network error is safe. - Units: amounts are decimals in the main currency unit (
149.90), VAT rate is a percentage (20), quantity is a decimal (1.5), dates areYYYY-MM-DDor ISO-8601. source: the permanent code of your system (^[a-z0-9][a-z0-9_-]{1,39}$). Several systems can send data to the same company with differentsourcevalues.- Full object: always send each record in full; a field that is not sent is treated as empty. Exception: in the
businessobject only the filled fields are written. - Order: within the same request, orders are processed before invoices; families are created before variants.
- Send only what changed: if nothing changed, do not send a request; the quota and rate limit protect the infrastructure shared by all companies.
Body
{
"source": "mystore",
"business": { "title": "…", "taxId": "…", "taxOffice": "…", "address": "…", "district": "…", "city": "…", "phone": "…", "email": "…",
"bankAccounts": [ { "bankName": "Ziraat Bankası", "branchName": "Merkez", "accountName": "…", "iban": "TR…", "currency": "TRY", "isDefault": true } ] },
"customers": [ /* Customer account */ ],
"suppliers": [ /* Supplier */ ],
"products": [ /* Product / variant */ ],
"stock": [ /* Stock level */ ],
"orders": [ /* E-commerce order */ ],
"invoices": [ /* Sales invoice */ ],
"expenses": [ /* Expense / incoming invoice */ ],
"transactions": [ /* Cash and bank movement */ ],
"payouts": [ /* Marketplace payout (optional) */ ],
"deleted": { "customers": [], "suppliers": [], "products": [], "expenses": [], "transactions": [] }
}Company (business)
Only the filled fields are written. bankAccounts[] (at most 10): matched by IBAN; the account is added both to the bank accounts shown on invoices and to Cash and banks tracking. The account with isDefault: true is shown first on invoices.
Customer account (customers[])
| Field | Type | Note |
|---|---|---|
externalId | text | required |
title | text | required; full name for an individual |
taxId | text | VKN (10 digits, tax number) or TCKN (11 digits, national ID number) |
taxOffice, address, district, city, country, postalCode, phone, email | text | |
contactType | customer | supplier | both | default customer |
heysirketId | text | matches a customer account created in Heyşirket (customer.created in the feed) to this externalId |
metadata | object | free-form |
Supplier (suppliers[])
| Field | Type | Note |
|---|---|---|
externalId, title | text | required |
taxId | text | VKN/TCKN — takes priority when matching |
contactName, email, phone, address, website | text | |
isActive | true/false | false deactivates |
heysirketId | text | matches a supplier created in Heyşirket (supplier.created in the feed) |
metadata | object | free-form |
If Heyşirket already has a supplier with the same tax number or the same name (case-insensitive, ignoring Turkish characters), no new one is created and the existing card is matched. Records in customers[] with contactType: "supplier" or "both" are also written as supplier cards. Every expense (expenses[]) is linked to its supplier by supplierExternalId → supplierTaxId → supplierName, in that order; if none is found, a supplier card is created automatically. This way supplier statements and payable balances build up in Heyşirket on their own.
Product and variant (products[])
| Field | Type | Note |
|---|---|---|
externalId, name | text | required |
sku, barcode | text | unique within the company |
description, brand, category, unit | text | unit defaults to Adet (piece) |
vatRate | number | percentage, default 20 |
price | number | selling price including VAT |
costPrice | number | unit purchase price excluding VAT. Heyşirket uses it to calculate order profit; if not sent, the existing value is kept |
currency | text | default TRY |
isActive, tracksInventory | true/false | isActive: false takes it off sale |
imageUrl, images[] | text | https images (the first is the main image, at most 10) |
family | object | parent product of a product with variants: externalId, name (required), description, brand, category, imageUrl, images[], isActive, heysirketId |
attributes | object | variant properties: { "Color": "Pink", "Size": "3-6 months" } |
heysirketId | text | matches a product/variant created in Heyşirket; no duplicate is created |
metadata | object | free-form |
For products with variants, only the sellable variants are sent as products[]; the parent product is described with family. In Heyşirket the family appears as a single row and its variants are edited in one window.
Stock level (stock[])
{ "productExternalId": "123", "quantity": 7, "warehouse": { "code": "ANA", "name": "Main warehouse" } } — send the absolute quantity; Heyşirket writes the difference to the stock ledger as a count adjustment. If warehouse is not sent, the default warehouse (or ANA if there is none) is used.
Sales invoice (invoices[])
| Field | Type | Note |
|---|---|---|
externalId, issueDate, total | required | |
number | text | invoice number |
dueDate | date | due date; for marketplace sales, the expected payout date. Defaults to issueDate |
documentType | E_INVOICE | E_ARCHIVE | PAPER | OTHER | |
status | APPROVED | SUBMITTED | REJECTED | CANCELLED | DRAFT | ERROR | CANCELLED for a cancellation |
isCreditNote | true/false | return (credit note) invoice |
currency, subtotal, vat | ||
customerExternalId / customer | the buyer (details at the time of the invoice) | |
einvoice | object | { "provider": "trendyol-efaturam", "uuid": "ETTN" } — if the company has an e-document connection, the original PDF is archived automatically |
order | object | { "channel": "trendyol", "externalOrderId": "…" } — the order's billing status becomes INVOICED |
lines[] | name required; quantity, unit, unitPrice (excluding VAT), vatRate, discount, vat, total (including VAT), sku, productExternalId | |
payments[] | externalId, amount, paidAt, method, note — the payment status is calculated from this; earlier payments not in the list are removed |
Expense / incoming invoice (expenses[])
| Field | Type | Note |
|---|---|---|
externalId, issueDate, total, supplierName | required | |
kind | EXPENSE | RECEIPT | INCOMING_E_INVOICE | OTHER | |
number, dueDate, currency, subtotal, vat, supplierTaxId, supplierExternalId | ||
category | text | e.g. KARGO (shipping), REKLAM (advertising), KOMISYON (commission), YAZILIM (software) |
paymentStatus, paid | UNKNOWN | UNPAID | PARTIAL | PAID (if you do not send a payment list) | |
payments[] | payments made to the supplier: externalId, amount, paidAt, method (BANK_TRANSFER | CASH | CREDIT_CARD | OFFSET | OTHER), note. When sent, it is taken as the full list: the paid amount and status are calculated from it and your earlier payments that are not in the list are removed; payments entered by hand in Heyşirket are not touched. OFFSET = set off against a marketplace payout (commission, shipping, service invoices) | |
einvoiceUuid | text | the ETTN of the incoming e-invoice; it is merged into a single record with the document Heyşirket fetches from the provider automatically. If the same externalId was sent earlier without an ETTN, the old record is moved onto this document (no duplicate expense) |
lines[], note, metadata |
Cash and bank movement (transactions[])
{ "externalId": "…", "direction": "INFLOW"|"OUTFLOW", "amount": 120.5, "currency": "TRY", "date": "2026-10-01", "description": "…", "counterparty": "…", "account": { "externalId": "iyzico", "name": "iyzico", "type": "BANK"|"CASH" }, "expenseExternalId": "…" } — do not send invoice collections here again (invoices[].payments is enough).
E-commerce order (orders[])
| Field | Type | Note |
|---|---|---|
channel | text | trendyol, hepsiburada, pazarama, n11, idefix, amazon-tr, woocommerce, shopify, ticimax, ideasoft, tiktok or your own code |
externalOrderId | text | required; for Trendyol, the package number (shipmentPackageId) |
orderNumber, orderDate | orderDate required | |
orderStatus | NEW | PENDING_APPROVAL | APPROVED | COMPLETED | CANCELLED | RETURNED | |
fulfillmentStatus | UNFULFILLED | READY_TO_SHIP | SHIPPED | DELIVERED | |
billingStatus | NOT_READY | READY | INVOICED | NOT_REQUIRED | |
currency, subtotal, shipping, discount, commission, total | ||
customer, shippingAddress, summary | object | |
items[] | externalLineId, sku, name, quantity, unitPrice, discount, vatRate, unitCost (optional: unit purchase cost excluding VAT at the time of sale; otherwise the purchase price on the product card), metadata.imageUrl (product image; otherwise the product card's image is used via the SKU) | |
shipments[] | carrier (the shipping company carrying the parcel), via (intermediary / shipping integrator, e.g. Kargonomi), trackingNumber, status, shippedAt, deliveredAt, trackingUrl (if missing, a tracking link is generated automatically for known carriers) | |
deductions | object | optional: the deductions the channel made for this order (including VAT) — commission, shipping, serviceFee, withholding, other; mark the ones not yet confirmed by a statement with estimated: ["serviceFee"] |
You do not send an order financial summary. Heyşirket calculates each order's sales VAT, product cost (products[].costPrice), marketplace deductions (deductions; for items that are not sent, from E-commerce → order details → Channel costs: commission rate and VAT treatment, service fee, fixed fee, per-package and low-value shipping, overheads, payment fee), per-unit operating cost, withholding, payout, profit before tax, profit tax and net profit by itself.
Marketplace payout (payouts[], optional — v1.6)
Reports a payment the marketplace made to you (payout / payment order) and which orders it covers. Heyşirket links each item to an order through orders[].externalOrderId (or orderNumber), then to that order's invoice through invoices[].order, and marks the invoice as "collected" in full on the payout date. The difference between the net payment and the invoice (commission, shipping, service fee, withholding, coupon) appears in the payout as deduction lines; the net amount is posted as a single entry to the bank/cash account (the account the company chose on the Payouts screen, or "<Channel> payout account" if none was chosen). The marketplace's commission/service invoices (e.g. DSM Grup for Trendyol) are treated as set off against this payout. If the same externalId is sent a second time it is updated; the collection is never written twice.
| Field | Type | Note |
|---|---|---|
externalId | text | required; the channel's payment ID (for Trendyol, paymentOrderId — it merges into the same record as the payout Heyşirket fetches from Trendyol itself) |
channel | text | required; the same code as orders[].channel |
status | PAID | EXPECTED | PAID (default): the money has arrived and the invoices are closed. EXPECTED: an expected payment, written only to the payout calendar |
payoutDate | date | required; the payment (or expected payment) date |
amount | number | the net amount credited to the account |
items[] | ≤1000 | orderNumber or externalOrderId (one of them is required), type: SALE | RETURN | DISCOUNT | COUPON | OTHER, gross (what the customer paid, write it as positive), net (what remains for the seller), deductions { commission, shipping, serviceFee, withholding, coupon, other }, externalId (recommended) |
deductions | object | deductions not tied to an order (same fields) |
Return (RETURN): if the order has a return invoice, that invoice is closed; otherwise the earlier payout collection is reversed. At most 20 payouts can be sent in one request (invoices+expenses+orders+payouts ≤150). The stats.payouts field of the response returns the collected (invoices collected) and unmatched (orders not matched) counts; unmatched ones can be matched by hand in Heyşirket on the E-commerce → Payouts screen.
{ "source": "mystore", "payouts": [ { "externalId": "112360", "channel": "trendyol", "payoutDate": "2026-10-14", "amount": 1873.42,
"items": [ { "externalId": "725041340", "externalOrderId": "4240252921", "type": "SALE", "gross": 799.99, "net": 653.40,
"deductions": { "commission": 133.60, "shipping": 0, "serviceFee": 12.99 } } ],
"deductions": { "withholding": 7.27 } } ] }Deletion (deleted)
Reported with lists of externalIds: customer accounts, suppliers and products are deactivated (history is kept); expenses and cash/bank movements from this source are deleted. Invoices are never deleted; send status: "CANCELLED".
Response and error codes
{
"ok": true,
"runId": "…",
"stats": { "customers": { "created": 3, "updated": 10 }, "products": { "created": 2, "updated": 40 } },
"errors": [ { "type": "products", "externalId": "55", "code": "DUPLICATE_SKU", "message": "Bu stok kodu (SKU) firmada başka bir ürüne ait." } ]
}An error in one record does not stop the others. Messages are in Turkish and can be shown to users; use code for logic.
Record code | Meaning | What to do |
|---|---|---|
VALIDATION | field missing/invalid | fix the field named in the message and send again |
DUPLICATE_SKU, DUPLICATE_BARCODE | SKU/barcode belongs to another product | send a unique code or leave the barcode empty |
DUPLICATE | uniqueness conflict | check the IDs |
REFERENCE | could not be written because of a linked record | send the linked record first |
NOT_FOUND | no product for the stock entry | send products[] first |
WRITE_FAILED | temporary write error | send the same page again |
| HTTP | code | What to do |
|---|---|---|
| 401 | API_KEY_INVALID | check the key / create a new one |
| 402 | SUBSCRIPTION_PAYMENT_REQUIRED | the company subscription is awaiting payment; stop |
| 413 | make the page smaller | |
| 422 | INVALID_BODY | fix the body (source, page limits) |
| 423 | BUSINESS_SUSPENDED | the company's access has been suspended; stop |
| 429 | RATE_LIMITED / DAILY_QUOTA | wait for Retry-After |
| 5xx | WRITE_FAILED | retry with exponential backoff |
Change feed (Heyşirket → your system)
GET /api/v1/changes?after=<seq>&limit=200[&types=inventory.movement,product.updated]
{ "events": [ { "seq": 41, "type": "inventory.movement", "entityType": "inventory_movement", "entityId": "…", "createdAt": "2026-10-13 09:12:00",
"data": { "productExternalProvider": "api:mystore", "productExternalId": "123", "sku": "S1", "direction": "IN", "quantity": 5, "warehouseCode": "ANA" } } ],
"next": 41, "hasMore": false }Only changes made in Heyşirket appear in the feed; anything written through this API is not echoed back (no loops). Store the next value and send it as after in the next call; if hasMore: true, call again right away. Match your own records with those whose externalProvider is api:<source>.
| Type | When | Key fields |
|---|---|---|
inventory.movement | stock in/out/count (including stock reduced by an invoice) | direction (IN +, OUT −, ADJUSTMENT signed, RESERVE/RELEASE informational), quantity, productExternalId, sku |
product.created / product.updated | product/variant added/changed (new size, price, taken off or put back on sale) | id, externalId (empty if created in Heyşirket), name, SKU, barcode, price, VAT, isActive, attributes, familyExternalId, imageUrl |
family.created / family.updated | product family added/changed/deactivated | id, externalId, name, description, brand, category, image, isActive |
customer.created / customer.updated | customer account added/changed | id, externalId, name, tax number, contact details |
supplier.created / supplier.updated | supplier added/changed | id, externalId, name, tax number, contact details, website, isActive |
invoice.created / invoice.updated | invoice issued in Heyşirket / status, number or payment status changed | id, externalId, number, status, providerId (ETTN), paymentStatus, amounts |
payment.created / payment.voided | payment recorded / voided | invoiceExternalId, amount, paidAt, method |
expense.created / expense.updated | expense added/changed | id, apiExternalId, amounts, supplier, category, payment status |
order.updated | e-commerce order approved/shipped/completed | channel, externalOrderId, action, statuses, trackingNumber |
Order of application: in every run, first read and apply the feed, then send your own changes. For a product/variant/family created in Heyşirket, create your own record and match it with heysirketId in the next send. Apply stock events as a difference; when you then send the absolute stock, Heyşirket sees a zero difference.
Integration checklist
- A separate key for each system; the key only in an environment variable.
GET /pingsucceeds.- All pages pass with
?dryRun=1without errors. - The first transfer is paged, can resume where it left off and respects the daily quota.
- Only changed records are sent (fingerprint / change time).
- Invoice line totals equal the invoice total; cancellations are
CANCELLED. - Variants are sent with
family+attributes, images over https. - The feed is read first, stock is applied as a difference,
nextis stored persistently. Retry-Afteris respected on 429, backoff is used on 5xx.- The last run and errors are visible on your admin screen and compared with Settings → API and sync in Heyşirket.
Frequently asked questions
What happens if I send the same record twice? It is updated; no duplicate is created.
My marketplace invoices look overdue. Send the expected payout date as dueDate; when the money arrives, report the payout with payouts[] (or let Heyşirket fetch payouts automatically through the Trendyol connection) — the invoices are then collected on their own.
My products show up one by one in Heyşirket. Send the variants with family and attributes; the family is grouped into a single row.
I entered stock in Heyşirket; how does it reach my system? Apply the inventory.movement event from the GET /changes feed as a difference.
I have a large catalogue. The first transfer is spread over a few days according to the daily quota; when you get DAILY_QUOTA, send the remaining records the next day.