Heyşirket · Developers
Two-way sync between store, warehouse and marketplace systems and Heyşirket

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


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

  1. 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.
  2. Test the connection.
    curl -s https://portal.heysirket.com/api/v1/ping -H "Authorization: Bearer $HEYSIRKET_API_KEY"
  3. Validate first, then write. Send the same body with ?dryRun=1; if errors is 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"}]}'
  4. 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
EndpointKeyDescription
GET /api/v1/metanot requiredlimits, constant values, version
GET /api/v1/openapi.jsonnot requiredOpenAPI 3.1 definition
GET /api/v1/pingrequiredthe company the key belongs to
POST /api/v1/syncrequiredbulk, idempotent transfer (?dryRun=1 only validates)
GET /api/v1/changes?after=0&limit=200requiredfeed of changes made in Heyşirket
GET /api/v1/sync/runs?limit=20requiredrecent transfers
LimitValue
Records per requestat most 400 (invoices + expenses + orders together at most 150)
Body sizeat most 2 MB
Transfer requests30 per minute per key (429 RATE_LIMITED, Retry-After: 60)
Daily record quota5,000 records per company per UTC day (429 DAILY_QUOTA; a large first transfer is spread over the following days)
Change feed retention30 days (read it at least once a day)

General rules

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[])

FieldTypeNote
externalIdtextrequired
titletextrequired; full name for an individual
taxIdtextVKN (10 digits, tax number) or TCKN (11 digits, national ID number)
taxOffice, address, district, city, country, postalCode, phone, emailtext
contactTypecustomer | supplier | bothdefault customer
heysirketIdtextmatches a customer account created in Heyşirket (customer.created in the feed) to this externalId
metadataobjectfree-form

Supplier (suppliers[])

FieldTypeNote
externalId, titletextrequired
taxIdtextVKN/TCKN — takes priority when matching
contactName, email, phone, address, websitetext
isActivetrue/falsefalse deactivates
heysirketIdtextmatches a supplier created in Heyşirket (supplier.created in the feed)
metadataobjectfree-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[])

FieldTypeNote
externalId, nametextrequired
sku, barcodetextunique within the company
description, brand, category, unittextunit defaults to Adet (piece)
vatRatenumberpercentage, default 20
pricenumberselling price including VAT
costPricenumberunit purchase price excluding VAT. Heyşirket uses it to calculate order profit; if not sent, the existing value is kept
currencytextdefault TRY
isActive, tracksInventorytrue/falseisActive: false takes it off sale
imageUrl, images[]texthttps images (the first is the main image, at most 10)
familyobjectparent product of a product with variants: externalId, name (required), description, brand, category, imageUrl, images[], isActive, heysirketId
attributesobjectvariant properties: { "Color": "Pink", "Size": "3-6 months" }
heysirketIdtextmatches a product/variant created in Heyşirket; no duplicate is created
metadataobjectfree-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[])

FieldTypeNote
externalId, issueDate, totalrequired
numbertextinvoice number
dueDatedatedue date; for marketplace sales, the expected payout date. Defaults to issueDate
documentTypeE_INVOICE | E_ARCHIVE | PAPER | OTHER
statusAPPROVED | SUBMITTED | REJECTED | CANCELLED | DRAFT | ERRORCANCELLED for a cancellation
isCreditNotetrue/falsereturn (credit note) invoice
currency, subtotal, vat
customerExternalId / customerthe buyer (details at the time of the invoice)
einvoiceobject{ "provider": "trendyol-efaturam", "uuid": "ETTN" } — if the company has an e-document connection, the original PDF is archived automatically
orderobject{ "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[])

FieldTypeNote
externalId, issueDate, total, supplierNamerequired
kindEXPENSE | RECEIPT | INCOMING_E_INVOICE | OTHER
number, dueDate, currency, subtotal, vat, supplierTaxId, supplierExternalId
categorytexte.g. KARGO (shipping), REKLAM (advertising), KOMISYON (commission), YAZILIM (software)
paymentStatus, paidUNKNOWN | 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)
einvoiceUuidtextthe 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[])

FieldTypeNote
channeltexttrendyol, hepsiburada, pazarama, n11, idefix, amazon-tr, woocommerce, shopify, ticimax, ideasoft, tiktok or your own code
externalOrderIdtextrequired; for Trendyol, the package number (shipmentPackageId)
orderNumber, orderDateorderDate required
orderStatusNEW | PENDING_APPROVAL | APPROVED | COMPLETED | CANCELLED | RETURNED
fulfillmentStatusUNFULFILLED | READY_TO_SHIP | SHIPPED | DELIVERED
billingStatusNOT_READY | READY | INVOICED | NOT_REQUIRED
currency, subtotal, shipping, discount, commission, total
customer, shippingAddress, summaryobject
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)
deductionsobjectoptional: 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.

FieldTypeNote
externalIdtextrequired; the channel's payment ID (for Trendyol, paymentOrderId — it merges into the same record as the payout Heyşirket fetches from Trendyol itself)
channeltextrequired; the same code as orders[].channel
statusPAID | EXPECTEDPAID (default): the money has arrived and the invoices are closed. EXPECTED: an expected payment, written only to the payout calendar
payoutDatedaterequired; the payment (or expected payment) date
amountnumberthe net amount credited to the account
items[]≤1000orderNumber 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)
deductionsobjectdeductions 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 codeMeaningWhat to do
VALIDATIONfield missing/invalidfix the field named in the message and send again
DUPLICATE_SKU, DUPLICATE_BARCODESKU/barcode belongs to another productsend a unique code or leave the barcode empty
DUPLICATEuniqueness conflictcheck the IDs
REFERENCEcould not be written because of a linked recordsend the linked record first
NOT_FOUNDno product for the stock entrysend products[] first
WRITE_FAILEDtemporary write errorsend the same page again
HTTPcodeWhat to do
401API_KEY_INVALIDcheck the key / create a new one
402SUBSCRIPTION_PAYMENT_REQUIREDthe company subscription is awaiting payment; stop
413make the page smaller
422INVALID_BODYfix the body (source, page limits)
423BUSINESS_SUSPENDEDthe company's access has been suspended; stop
429RATE_LIMITED / DAILY_QUOTAwait for Retry-After
5xxWRITE_FAILEDretry 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>.

TypeWhenKey fields
inventory.movementstock in/out/count (including stock reduced by an invoice)direction (IN +, OUT −, ADJUSTMENT signed, RESERVE/RELEASE informational), quantity, productExternalId, sku
product.created / product.updatedproduct/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.updatedproduct family added/changed/deactivatedid, externalId, name, description, brand, category, image, isActive
customer.created / customer.updatedcustomer account added/changedid, externalId, name, tax number, contact details
supplier.created / supplier.updatedsupplier added/changedid, externalId, name, tax number, contact details, website, isActive
invoice.created / invoice.updatedinvoice issued in Heyşirket / status, number or payment status changedid, externalId, number, status, providerId (ETTN), paymentStatus, amounts
payment.created / payment.voidedpayment recorded / voidedinvoiceExternalId, amount, paidAt, method
expense.created / expense.updatedexpense added/changedid, apiExternalId, amounts, supplier, category, payment status
order.updatede-commerce order approved/shipped/completedchannel, 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

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.