On this page

Developers

Genel Kurye POS API

This API lets a POS or order system send a restaurant's orders to the Genel Kurye courier pool for delivery.

Example: create a delivery. The response is shortened.

POST /api/pos/v1/deliveries HTTP/1.1
Host: api.genelkuryerkooperatifi.org
X-Partner-Key: <partner key>
X-Store-Id: 48213907
X-Api-Key: gk_XXXXXXXXXXXXXXXXXXXXXXXX
X-Api-Secret: gks_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Content-Type: application/json

{
  "external_order_id": "SP-100234",
  "platform": "yemeksepeti",
  "platform_order_code": "Y7K2",
  "customer": { "name": "Ayşe Yılmaz", "phone": "0532 111 22 33" },
  "address": {
    "line": "Caferağa Mah. Moda Cad. No:12",
    "district": "Kadıköy",
    "city": "İstanbul",
    "lat": 40.98712,
    "lng": 29.02631
  },
  "payment": { "method": "cash", "amount_to_collect": 245.5 }
}
Response
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8

{
  "id": "b1tqwff2g8n4kpg36kbhhmcf",
  "code": "KRY-6BEJZ3",
  "external_order_id": "SP-100234",
  "status": "pending",
  "courier": null,
  "pickup_code": null
}

It is published by S.S. Genel Kurye Kooperatifi (the cooperative).

Terms used in this document:

  • Order: the order in the POS. Delivery: the courier job Genel Kurye opens for an order. The Turkish messages say sipariş for order and teslimat for delivery.
  • Store: one restaurant's connection to your POS, identified by its Store ID.
  • Keys: a restaurant's Store ID, API Key and API Secret, together.
  • Pool: where open deliveries wait for a courier.
  • Platform courier: a courier of the marketplace the order came from, for example a Yemeksepeti courier, who delivers the order instead of a Genel Kurye courier.

Contact: iletisim@genelkuryerkooperatifi.org

Plain Markdown: https://www.genelkuryerkooperatifi.org/developers.md

Last updated 2 November 2026 · v1

View as Markdown

Endpoints at a glance

Basics

How it works

A restaurant connects its POS with three values, and from then on the POS sends its orders to Genel Kurye.

  1. In the Genel Kurye panel, the restaurant opens the page for your POS under Ayarlar → Entegrasyonlar (Settings → Integrations) and creates its keys: a Store ID, an API Key and an API Secret.
  2. The restaurant enters the keys in the Genel Kurye fields in the POS. When the POS saves them, it can check them with GET /store.
  3. When the restaurant sends an order to Genel Kurye, the POS calls POST /deliveries. The delivery appears in the cooperative's courier pool.
  4. As the delivery's status changes, Genel Kurye sends signed webhook events to the webhook URL registered for the POS. The POS can also read the status with GET /deliveries/{id}.
  5. If the restaurant cancels the order, the POS calls POST /deliveries/{id}/cancel.

If the POS sends every order of the restaurant, including the ones a platform courier delivers, it marks those with "delivery_by": "platform". Genel Kurye then records the order and sends no courier (see Platform deliveries).

The pickup address is always the address saved in the restaurant's Genel Kurye profile. The POS sends only the drop-off address.

Basics

Getting access

The cooperative sets up your access by hand.

Write to iletisim@genelkuryerkooperatifi.org. The cooperative sends you these through a secure channel:

  • a test store: a Store ID, an API Key and an API Secret,
  • your partner key,
  • your webhook secret.

Send the cooperative your webhook URL. It must start with https:// and must not contain a user name or password. One URL serves all your stores. Events are not sent to any other kind of URL.

Basics

Base URL and format

Every request goes to one base URL, over HTTPS, with JSON bodies.

SettingValue
Base URLhttps://api.genelkuryerkooperatifi.org/api/pos/v1
ProtocolHTTPS only
BodyJSON, UTF-8, Content-Type: application/json
TimeISO 8601. Times in responses are UTC (2026-10-17T10:30:00.000Z). Always include a time zone (+03:00 or Z) in the times you send

Paths in the endpoint reference are relative to the base URL. The examples show the full request. A path or method that is not in the reference gets 404 unknown_endpoint. The one exception is a body that is not valid JSON or is larger than 64 KB: it gets the 400 or 413 described at the end of Errors, whatever the path.

Basics

Authentication

Every request carries four headers: one from the POS company and three from the restaurant.

Every example in this reference shows all four.

  • X-Partner-Keystringrequired

    One key per POS company, the same for all its restaurants. The cooperative gives it to the POS company. Keep it on your server and never show it to the restaurant. Never put it in a desktop or mobile build. When the cooperative changes the key, the old key and the new key both work until the cooperative turns the old one off. Example: 5f0c…

  • X-Store-Idstringrequired

    The restaurant's Store ID: 8 digits, not starting with 0. Example: 48213907

  • X-Api-Keystringrequired

    The restaurant's API Key: gk_ followed by 24 letters or digits. Example: gk_XXXXXXXXXXXXXXXXXXXXXXXX

  • X-Api-Secretstringrequired

    The restaurant's API Secret: gks_ followed by 40 letters or digits

  • The partner key makes sure a restaurant's keys work only from this POS. If it is missing or wrong, the response is 401 unauthorized.
  • The restaurant's three values are checked together. If any one is wrong, the response is 401 unauthorized, and it does not say which.
  • The restaurant can see its API Secret again at any time in the Genel Kurye panel. Genel Kurye stores the secret encrypted and checks each request against its SHA-256 hash.
  • If the restaurant creates new keys in the panel, the old API Key and API Secret stop working immediately. The Store ID does not change.
  • If the restaurant removes the connection, its keys are deleted and every later request gets 401. A restaurant that reconnects gets a new Store ID. Order IDs are unique within a store, so the same external_order_id opens a new delivery under the new Store ID.

Basics

Testing

Test against the same base URL, with a test store.

There is no separate test server. The cooperative opens a test store for your engineering team and sends you its keys.

Every order sent to the test store must include "test": true. An order without it is rejected with 403 test_order_required and nothing about its customer is stored, so if the test keys are ever entered in a real restaurant's POS by mistake, no real customer data is accepted. A real store rejects "test": true with 400 invalid_request.

Use drop-off addresses within 50 km of the test store's pickup address. An order farther away is rejected with 422 address_too_far.

Deliveries opened by the test store are visible only to test couriers, so no real courier is sent. The cooperative's test couriers can accept these deliveries and move them through every status, so you receive every status event. Ask the cooperative to move a test delivery forward when you want to see a status event.

Basics

Errors

Errors share one JSON shape, with one exception described at the end of this section.

Response400 Bad Request
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
Cache-Control: no-store

{
  "error": {
    "code": "invalid_request",
    "message": "payment.method şunlardan biri olmalı: online, cash, card, meal_card.",
    "field": "payment.method"
  }
}
  • code is stable. Branch on it in your code.
  • message is Turkish text that you can show to the restaurant unchanged. Do not parse it.
  • field is present only for errors about a field in the body or about the external_order_id query parameter.

Error codes

The Message column shows one sentence the server sends with each code. Some codes also have other sentences.

  • 400

    The body or a field is invalid (field says which)

    Message: payment.method şunlardan biri olmalı: online, cash, card, meal_card.

  • 401

    Headers are missing, the partner key is wrong, or the Store ID, API Key and API Secret do not match

    Message: Store ID, API Key ya da API Secret yanlış.

  • 403

    The restaurant's Genel Kurye account is closed. Normally the keys are removed with the account, and requests get 401 instead

    Message: İşletmenin Genel Kurye hesabı kapalı.

  • 403

    The restaurant paused the connection in the panel. POST /deliveries is refused; the other endpoints still work

    Message: İşletme Genel Kurye bağlantısını duraklatmış; sipariş alınmadı.

  • 403

    The cooperative has not approved this restaurant for order transfer yet

    Message: Kooperatif bu işletme için sipariş aktarımını henüz açmadı; sipariş alınmadı.

  • 403

    The restaurant's profile is missing a pickup address, province, location or business type

    Message: İşletmenin Genel Kurye profilinde alış adresi, ili, konumu ya da işletme türü eksik; sipariş alınmadı.

  • 403

    The connection was stopped. The restaurant must create new keys

    Message: Bu bağlantı durdurulmuş. İşletme Genel Kurye panelinden yeni anahtar oluşturmalı.

  • 403

    An order was sent to a test store without "test": true

    Message: Test mağazası yalnızca "test": true işaretli siparişleri alır; sipariş kaydedilmedi.

  • 404

    There is no such delivery for this restaurant

    Message: Bu mağazada böyle bir teslimat yok.

  • 404

    The path is not an endpoint of this API, or the endpoint does not take this method. The headers do not change this response, but a body that is not valid JSON gets the 400 described below first. Check the base URL, the path and the method

    Message: Geçersiz endpoint: istek gönderdiğiniz yol ya da HTTP yöntemi POS API'de yok. API dokümantasyonunu kontrol edin.

  • 409

    The delivery is delivered, so it cannot be cancelled

    Message: Teslimat tamamlanmış; iptal edilemez.

  • 409

    The package cannot be handed over: no courier is assigned yet (pending), the delivery is cancelled, returning or returned, or a platform courier delivers it (platform)

    Message: Bu teslimata henüz kurye atanmadı.

  • 409

    A platform status was sent for a delivery that is not platform

    Message: Bu teslimatı platform kuryesi götürmüyor; platform durumu yazılamaz.

  • 422

    address.lat and address.lng are missing or outside Turkey, and the address lookup found nothing or did not answer (see Location)

    Message: Teslim adresi haritada bulunamadı; teslimat açılmadı. Teslimatı Genel Kurye panelindeki Yeni teslimat formundan elle açın.

  • 422

    The drop-off point is more than 50 km from the pickup address, and the address lookup found no point within 50 km (see Location). field is address. The distance is in message

    Message: Teslim adresi alış adresine 50 km'den uzak (yaklaşık 530 km); teslimat açılmadı. Adresi ve işletmenin Genel Kurye profilindeki alış adresini kontrol edin.

  • 422

    The delivery could not be opened. The reason is in message

    Message: İşletme türün artık kullanılmıyor. Profil'de güncelle.

  • 429

    The per-minute request limit was exceeded (see the Retry-After header)

    Message: Çok fazla istek gönderildi. Biraz sonra tekrar deneyin.

  • 429

    The hourly delivery limit was exceeded (Retry-After)

    Message: Bu mağaza için son bir saatte açılabilecek teslimat sınırına ulaşıldı. Biraz sonra tekrar deneyin.

  • 429

    The store used up its address lookups for orders that need one (Retry-After)

    Message: Bu mağaza için adres arama sınırı doldu; teslimat açılmadı. Biraz sonra tekrar deneyin ya da teslimatı Genel Kurye panelinden elle açın.

  • 500

    Unexpected error. Send the same request again

    Message: Beklenmeyen bir hata oldu. Biraz sonra tekrar deneyin.

  • 503

    The address lookup is busy (Retry-After)

    Message: Adres şu an konuma çevrilemiyor. Birkaç saniye sonra tekrar deneyin.

Basics

Limits

Limits apply to each store. The body size applies to every request.

LimitValueWhen exceeded
Request body64 KB413, in the server's own format (see the end of Errors)
Requests120 per minute429 rate_limited
New deliveries100 per hour429 hourly_limit
Address lookupsFor orders that need one (Location): 20 per 10 minutes, retries included429 geocode_limit. 503 busy if the store already has a lookup running, or if the wait for a free lookup slot would pass 3 seconds

Endpoints

Check the connection

Confirms that the keys work and tells you whether the store is accepting orders right now.

GET/store
GET /api/pos/v1/store HTTP/1.1
Host: api.genelkuryerkooperatifi.org
X-Partner-Key: <partner key>
X-Store-Id: 48213907
X-Api-Key: gk_XXXXXXXXXXXXXXXXXXXXXXXX
X-Api-Secret: gks_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Response
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Cache-Control: no-store

{
  "store_id": "48213907",
  "name": "Örnek Restoran",
  "provider": "ornek-pos",
  "accepting_orders": true,
  "reason": null,
  "test": false
}

Call it when the restaurant saves the keys on your settings screen.

name is the restaurant's name in Genel Kurye, or null. provider is the ID of your POS at Genel Kurye. test is true for a test store.

If accepting_orders is false, reason is one of paused, not_approved or profile_incomplete. These are the codes POST /deliveries returns in the same situations.

Endpoints

Create a delivery

Opens a delivery for an order.

POST/deliveries
POST /api/pos/v1/deliveries HTTP/1.1
Host: api.genelkuryerkooperatifi.org
X-Partner-Key: <partner key>
X-Store-Id: 48213907
X-Api-Key: gk_XXXXXXXXXXXXXXXXXXXXXXXX
X-Api-Secret: gks_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Content-Type: application/json

{
  "external_order_id": "SP-100234",
  "platform": "yemeksepeti",
  "platform_order_code": "Y7K2",
  "ordered_at": "2026-10-17T10:30:00+03:00",
  "prep_minutes": 15,
  "customer": {
    "name": "Ayşe Yılmaz",
    "phone": "0532 111 22 33"
  },
  "address": {
    "line": "Caferağa Mah. Moda Cad. No:12",
    "neighborhood": "Caferağa",
    "district": "Kadıköy",
    "city": "İstanbul",
    "building": "12",
    "floor": "2",
    "apartment": "5",
    "directions": "Eczanenin üstü",
    "lat": 40.98712,
    "lng": 29.02631
  },
  "payment": {
    "method": "cash",
    "amount_to_collect": 245.5
  },
  "items": [
    { "name": "Lahmacun", "quantity": 2 },
    { "name": "Ayran", "quantity": 2 }
  ],
  "note": "Zile basmayın"
}
Response
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Cache-Control: no-store

{
  "id": "b1tqwff2g8n4kpg36kbhhmcf",
  "code": "KRY-6BEJZ3",
  "store_id": "48213907",
  "external_order_id": "SP-100234",
  "status": "pending",
  "test": false,
  "courier": null,
  "pickup_code": null,
  "cancel_reason": null,
  "fail_reason": null,
  "created_at": "2026-10-17T07:56:33.953Z",
  "assigned_at": null,
  "picked_up_at": null,
  "delivered_at": null,
  "cancelled_at": null,
  "returned_at": null,
  "delivery_by": "restaurant",
  "platform_status": null,
  "platform_status_at": null
}
{
  "external_order_id": "SP-100235",
  "platform": "yemeksepeti",
  "platform_order_code": "Q4M8",
  "delivery_by": "platform",
  "payment": { "method": "online" },
  "items": [{ "name": "Lahmacun", "quantity": 2 }]
}

Body parameters

Free text longer than its limit is cut. An identifier or a code that is too long is rejected with 400 invalid_request.

  • external_order_idstringrequired

    The order's ID in the POS: 1–64 printable ASCII characters, no spaces. Turkish letters and other non-ASCII characters are rejected. Unique within a store

  • testbooleanrequired in a test store

    true marks a test order. Send it only to a test store: a real store returns 400 invalid_request

  • platformstring

    Where the order came from: yemeksepeti, trendyol, getir, migros, tiklagelsin, phone, web, or another short name of up to 40 characters (a–z, digits, -, _). Turkish letters are converted to a–z

  • platform_order_codestring

    A short code the courier uses to match the package: the platform's order code or the receipt number. Up to 20 printable ASCII characters. Turkish letters are rejected

  • brandstring

    The brand the order was sold under, when one restaurant sells under several brands from one kitchen, for example Beast Burger. Couriers see it next to the order code and use it to find the right package at the counter. Up to 60 characters

  • ordered_atstring

    When the order was placed, with a time zone: 2026-10-17T10:30:00+03:00

  • prep_minutesinteger

    The kitchen's estimated preparation time, 0–240. The courier sees it as "about 15 minutes"

  • package_countinteger

    Number of packages, 1–50. Default 1

  • delivery_bystring

    Who delivers the order: restaurant (a Genel Kurye courier) or platform (a platform courier, see Platform deliveries). Default restaurant; an empty string counts as not sent. Upper and lower case are the same. Any other value returns 400 invalid_request with field set to delivery_by

  • customer.namestring

    Shown to the courier as "Ayşe Y." once the courier accepts the delivery. Up to 120 characters

  • customer.phonestring

    7–15 digits, up to 30 characters in all. + is allowed only at the start; spaces, - and parentheses are allowed. A masked number from a platform is accepted, also as one line with the code to dial after a slash: 0212 365 34 03 / 117 678 795 07. A phone that is not a number does not refuse the order: the courier sees it as sent, once the package is picked up

  • customer.phone_codestring

    A code to dial after a masked number connects: up to 20 digits, * and #. Ignored without customer.phone

  • address.linestringrequired unless delivery_by is platform

    Street and door number, for example Moda Cad. No:12. Up to 300 characters

  • address.neighborhoodstring

    Neighborhood. Up to 80 characters

  • address.districtstring

    District. Up to 60 characters

  • address.citystring

    City or province. Up to 60 characters

  • address.buildingstring

    Building. Up to 30 characters

  • address.floorstring

    Floor. Up to 30 characters

  • address.apartmentstring

    Apartment. Up to 30 characters

  • address.directionsstring

    Directions to the address. Up to 300 characters

  • address.latnumberrecommended

    Latitude of the drop-off point. Send it together with address.lng: sending only one returns 400 invalid_request

  • address.lngnumberrecommended

    Longitude of the drop-off point. A latitude and longitude of 0 and 0 count as no location

  • payment.methodstring

    online (already paid), cash (cash at the door), card (card at the door) or meal_card (meal card at the door)

  • payment.amount_to_collectnumber

    Amount to collect at the door, in ₺: from 0 to less than 1,000,000. Ignored for online. An amount above 0 requires payment.method

  • itemsarray

    The order's lines. Only the first 30 are stored. Item names are visible to couriers while the delivery is in the pool. A note inside an item is ignored

  • items[].namestringrequired

    The item's name. Up to 120 characters

  • items[].quantityinteger

    1–99. Default 1

  • notestring

    The customer's note. Up to 300 characters

Location

Always send the location that the platform gives you. A location outside Turkey (latitude 35–43, longitude 25–45) counts as no location.

Genel Kurye looks the address up on a map when there is no location, or when the location is more than 50 km in a straight line from the pickup address. The lookup is slow and does not find every address. It is limited to 20 orders per store in 10 minutes. Every order that needs a lookup counts, retries included.

  • If there is no location, and the lookup finds nothing or does not answer, the response is 422 address_not_found.
  • If the location you sent, or the point the lookup found, is more than 50 km from the pickup address, and the lookup found no point within 50 km, the response is 422 address_too_far. message gives the distance in whole kilometres, rounded up: the distance of the location you sent if it was too far, otherwise that of the first point the lookup found. Check the drop-off address. Also check the pickup address in the restaurant's Genel Kurye profile.

Genel Kurye remembers what a lookup found for 10 minutes and does not look the same address up again during that time. The remembered point is compared with the restaurant's current pickup address on every request, so a corrected pickup address takes effect at once.

Response

A new delivery returns 201 Created. The body is a delivery object.

Sending the same order again

A second request with the same external_order_id does not open a new delivery. It returns the delivery the first request opened, with 200 OK, and ignores the other fields in the body. If a request times out or fails on the network, send the same request again. If the order changes, cancel the delivery and send it again with a new external_order_id.

Platform deliveries

Some orders are delivered by a platform courier, not by Genel Kurye. If your POS sends all of a restaurant's orders, send these with "delivery_by": "platform".

  • The response is 201 Created with status platform and delivery_by platform. No courier is sent, the address is not looked up, and the delivery never appears in the pool.
  • Only the order's own fields are read: external_order_id, test, platform, platform_order_code, brand, ordered_at, prep_minutes, package_count, payment, items, address.district and address.city. customer, the rest of address and note are not needed. If you send them, they are ignored and not stored.
  • The restaurant sees the order in its Genel Kurye panel, marked as a platform delivery.
  • platform is final. No webhook is sent for it.
  • To pass on what the platform courier does, call POST /deliveries/{id}/platform-status.
  • An order sent without delivery_by gets a Genel Kurye courier. If the restaurant tells the cooperative that a platform courier took it, the cooperative can move the delivery to platform while it is still pending. Your POS sees the change on its next GET /deliveries/{id}. No webhook is sent.
  • The new-delivery limit (100 per hour) counts platform deliveries too.

Endpoints

Get a delivery

Returns one delivery by its ID.

GET/deliveries/{id}
GET /api/pos/v1/deliveries/b1tqwff2g8n4kpg36kbhhmcf HTTP/1.1
Host: api.genelkuryerkooperatifi.org
X-Partner-Key: <partner key>
X-Store-Id: 48213907
X-Api-Key: gk_XXXXXXXXXXXXXXXXXXXXXXXX
X-Api-Secret: gks_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Response
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Cache-Control: no-store

{
  "id": "b1tqwff2g8n4kpg36kbhhmcf",
  "code": "KRY-6BEJZ3",
  "store_id": "48213907",
  "external_order_id": "SP-100234",
  "status": "assigned",
  "test": false,
  "courier": { "name": "Ahmet Kaya", "phone": "0532 123 45 67" },
  "pickup_code": "482913",
  "cancel_reason": null,
  "fail_reason": null,
  "created_at": "2026-10-17T07:56:33.953Z",
  "assigned_at": "2026-10-17T08:03:12.418Z",
  "picked_up_at": null,
  "delivered_at": null,
  "cancelled_at": null,
  "returned_at": null,
  "delivery_by": "restaurant",
  "platform_status": null,
  "platform_status_at": null
}

You can only read deliveries that this restaurant opened through your POS. The ID of any other delivery returns 404 not_found.

Path parameters

  • idstringrequired

    The id that POST /deliveries returned

Endpoints

Find a delivery by order ID

Looks up a delivery by the POS's own order ID.

GET/deliveries
GET /api/pos/v1/deliveries?external_order_id=SP-100234 HTTP/1.1
Host: api.genelkuryerkooperatifi.org
X-Partner-Key: <partner key>
X-Store-Id: 48213907
X-Api-Key: gk_XXXXXXXXXXXXXXXXXXXXXXXX
X-Api-Secret: gks_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Response
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Cache-Control: no-store

{ "data": [ { "id": "b1tqwff2g8n4kpg36kbhhmcf", "status": "assigned" } ] }

The response is a list with at most one item. The example is shortened: each item in the list is a full delivery object. If there is no such order, the response is { "data": [] }.

Query parameters

  • external_order_idstringrequired

    The order's ID in the POS, as sent to POST /deliveries. Without a valid ID the response is 400 invalid_request with field set to external_order_id

Endpoints

Cancel a delivery

Cancels a delivery. If the courier already has the package, they bring it back to the restaurant. On a platform delivery, it records that the order was cancelled on the platform.

POST/deliveries/{id}/cancel
POST /api/pos/v1/deliveries/b1tqwff2g8n4kpg36kbhhmcf/cancel HTTP/1.1
Host: api.genelkuryerkooperatifi.org
X-Partner-Key: <partner key>
X-Store-Id: 48213907
X-Api-Key: gk_XXXXXXXXXXXXXXXXXXXXXXXX
X-Api-Secret: gks_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Content-Type: application/json

{ "reason": "Müşteri vazgeçti" }
Response
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Cache-Control: no-store

{
  "id": "b1tqwff2g8n4kpg36kbhhmcf",
  "code": "KRY-6BEJZ3",
  "store_id": "48213907",
  "external_order_id": "SP-100234",
  "status": "cancelled",
  "test": false,
  "courier": null,
  "pickup_code": null,
  "cancel_reason": "Sipariş Örnek POS'ta iptal edildi (Müşteri vazgeçti).",
  "fail_reason": null,
  "created_at": "2026-10-17T07:56:33.953Z",
  "assigned_at": null,
  "picked_up_at": null,
  "delivered_at": null,
  "cancelled_at": "2026-10-17T07:58:02.771Z",
  "returned_at": null,
  "delivery_by": "restaurant",
  "platform_status": null,
  "platform_status_at": null
}

Path parameters

  • idstringrequired

    The id that POST /deliveries returned

Body parameters

  • reasonstring

    Why the order was cancelled. Genel Kurye puts it into a fixed sentence, which the courier and the restaurant see: Sipariş Örnek POS'ta iptal edildi (Müşteri vazgeçti). (in English: "Order cancelled in Örnek POS (customer changed their mind)."). The sentence names your POS. Your reason goes between the brackets unchanged, so send a short Turkish phrase, not a code such as customer_cancelled. Without a reason the brackets are left out. Text longer than 160 characters is cut

Result by status

Delivery statusResult
pending, assignedcancelled. If a courier is assigned, they are notified
picked_up, in_transitreturning. The courier already has the package, so they bring it back to the restaurant, and then the delivery becomes returned
cancelled, returning, returnedNo change. The response is 200 with the unchanged delivery
delivered409 not_cancellable
platformstatus stays platform and platform_status becomes cancelled, as with Report platform progress. reason is not stored. If platform_status is already delivered or cancelled, nothing changes. The response is 200 either way

Endpoints

Hand over a package

Tells Genel Kurye that the restaurant gave the package to the courier.

POST/deliveries/{id}/handover
POST /api/pos/v1/deliveries/b1tqwff2g8n4kpg36kbhhmcf/handover HTTP/1.1
Host: api.genelkuryerkooperatifi.org
X-Partner-Key: <partner key>
X-Store-Id: 48213907
X-Api-Key: gk_XXXXXXXXXXXXXXXXXXXXXXXX
X-Api-Secret: gks_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Response
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Cache-Control: no-store

{
  "id": "b1tqwff2g8n4kpg36kbhhmcf",
  "code": "KRY-6BEJZ3",
  "store_id": "48213907",
  "external_order_id": "SP-100234",
  "status": "picked_up",
  "test": false,
  "courier": { "name": "Ahmet Kaya", "phone": "0532 123 45 67" },
  "pickup_code": null,
  "cancel_reason": null,
  "fail_reason": null,
  "created_at": "2026-10-17T07:56:33.953Z",
  "assigned_at": "2026-10-17T08:03:12.418Z",
  "picked_up_at": "2026-10-17T08:12:40.120Z",
  "delivered_at": null,
  "cancelled_at": null,
  "returned_at": null,
  "delivery_by": "restaurant",
  "platform_status": null,
  "platform_status_at": null
}

This is the same as the "I gave the package to the courier" button in the restaurant's panel. Use it when the courier cannot enter the pickup code, for example because their phone is off.

Path parameters

  • idstringrequired

    The id that POST /deliveries returned

Result by status

Delivery statusResult
assignedpicked_up
picked_up, in_transit, deliveredNo change. The response is 200 with the unchanged delivery
pending409 not_assigned: no courier is assigned yet
cancelled, returning, returned409 not_assigned
platform409 not_assigned: a platform courier delivers it, so no Genel Kurye courier is assigned. The message is Bu teslimatı platform kuryesi götürüyor; paketin alındığını POST /deliveries/:id/platform-status ile bildirin.

Endpoints

Report platform progress

Tells Genel Kurye what the platform courier did with a platform delivery.

POST/deliveries/{id}/platform-status
POST /api/pos/v1/deliveries/c7rmx2kq9w4hd8tjz3pn5vfa/platform-status HTTP/1.1
Host: api.genelkuryerkooperatifi.org
X-Partner-Key: <partner key>
X-Store-Id: 48213907
X-Api-Key: gk_XXXXXXXXXXXXXXXXXXXXXXXX
X-Api-Secret: gks_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Content-Type: application/json

{ "status": "picked_up", "occurred_at": "2026-10-17T10:52:00+03:00" }
Response
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Cache-Control: no-store

{
  "id": "c7rmx2kq9w4hd8tjz3pn5vfa",
  "code": "KRY-Q2W7ZA",
  "store_id": "48213907",
  "external_order_id": "SP-100235",
  "status": "platform",
  "test": false,
  "courier": null,
  "pickup_code": null,
  "cancel_reason": null,
  "fail_reason": null,
  "created_at": "2026-10-17T07:41:09.512Z",
  "assigned_at": null,
  "picked_up_at": null,
  "delivered_at": null,
  "cancelled_at": null,
  "returned_at": null,
  "delivery_by": "platform",
  "platform_status": "picked_up",
  "platform_status_at": "2026-10-17T07:52:00.000Z"
}

The restaurant sees it in its panel. Nothing else changes: the delivery stays platform, and no webhook is sent. Calling it is optional.

Path parameters

  • idstringrequired

    The id that POST /deliveries returned

Body parameters

  • statusstringrequired

    picked_up (the platform courier took the package), delivered (the platform courier delivered it) or cancelled (the order was cancelled on the platform). Upper and lower case are the same. A missing or unknown value returns 400 invalid_request with field set to status

  • occurred_atstring

    When it happened, with a time zone: 2026-10-17T10:52:00+03:00. Without it, the time Genel Kurye received the request. A time in the future counts as now. A value that is not a time returns 400 invalid_request with field set to occurred_at

Result by status

The platform status only moves forward: picked_up, then delivered or cancelled. You can skip picked_up. delivered and cancelled are final.

DeliveryResult
platform, and the new status moves forwardplatform_status and platform_status_at are set. The response is 200
platform, and the new status does not move forward (the same status again, or picked_up after delivered)No change. The response is 200 with the unchanged delivery
Any other status409 not_platform

Reference

Delivery object

The delivery as every endpoint and every webhook event returns it.

  • idstring

    The delivery's ID in Genel Kurye

  • codestring

    The delivery's code (KRY-…). The courier and the restaurant see this code

  • store_idstring

    The Store ID, the same value as X-Store-Id

  • external_order_idstring

    The ID the POS sent

  • statusstring

    One of the statuses

  • testboolean

    Whether the delivery belongs to a test store

  • courierobject | null

    { "name", "phone" }. phone is the courier's own number. See Personal data. null until a courier is assigned

  • pickup_codestring | null

    The 6-digit pickup code. null unless the status is assigned

  • cancel_reasonstring | null

    Why the delivery was cancelled, including a cancel after pickup

  • fail_reasonstring | null

    Why a package came back undelivered: not_home, wrong_address, refused, phone_unreachable or other. Today it is always null for deliveries opened through this API

  • created_atstring

    When the delivery was opened

  • assigned_atstring | null

    When a courier accepted the delivery

  • picked_up_atstring | null

    When the courier took the package

  • delivered_atstring | null

    When the package was delivered

  • cancelled_atstring | null

    When the delivery was cancelled

  • returned_atstring | null

    When the package was brought back to the restaurant

  • delivery_bystring

    platform when a platform courier delivers the order (status platform), otherwise restaurant

  • platform_statusstring | null

    The last platform status you reported: picked_up, delivered or cancelled. null until you report one, and on every delivery that is not platform

  • platform_status_atstring | null

    When that platform status happened

A time is null until its step happens. There is no time field for in_transit or returning.

Pickup code

When the courier arrives at the restaurant, the restaurant tells them this 6-digit code, and they enter it in their app to take the package. The Genel Kurye panel shows the code to the restaurant. The POS receives it in the delivery.assigned event and should show it to the restaurant staff.

Reference

Statuses

A delivery moves through these statuses.

statusMeaningSuggested status in your POS
pendingIn the pool, waiting for a courierHazırlanıyor (preparing)
assignedA courier accepted the delivery and is coming to the restaurantHazırlanıyor (preparing)
picked_upThe courier has the packageYolda (on the way)
in_transitThe courier is heading to the customerYolda (on the way)
deliveredThe package was handed to the customerTeslim Edildi (delivered)
cancelledCancelled before pickup. The package is still at the restaurantİptal (cancelled)
returningCancelled after pickup, by the restaurant or by the cooperative. The package is on its way back to the restaurantİade (returned), or İptal if there is no equivalent
returnedThe package was brought back to the restaurantİade (returned), or İptal if there is no equivalent
platformA platform courier delivers the order. Genel Kurye sends no courier. Never sent as a webhook; read it with Get a deliveryThe platform's own status, or platform_status if you report it

A delivery moves through pending → assigned → picked_up → in_transit → delivered. It can become cancelled only before the package is picked up, and returning or returned only after.

A delivery is platform from the start when you send "delivery_by": "platform". A pending delivery can also become platform when the cooperative learns that a platform courier took the order. A platform delivery never changes status again.

Reference

Webhooks

Genel Kurye sends a signed POST to your webhook URL when the status of one of your deliveries changes.

{
  "event_id": "evt_b1tqwff2g8n4kpg36kbhhmcf_picked_up_17",
  "type": "delivery.picked_up",
  "occurred_at": "2026-10-17T08:12:40.120Z",
  "delivery": {
    "id": "b1tqwff2g8n4kpg36kbhhmcf",
    "code": "KRY-6BEJZ3",
    "store_id": "48213907",
    "external_order_id": "SP-100234",
    "status": "picked_up",
    "courier": { "name": "Ahmet Kaya", "phone": "0532 123 45 67" },
    "pickup_code": null,
    "picked_up_at": "2026-10-17T08:12:40.120Z"
  }
}
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody, timestamp, signature, secret) {
  const age = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (!Number.isFinite(age) || age > 300) return false;
  const expected = "sha256=" + createHmac("sha256", secret)
    .update(`${timestamp}.`)
    .update(rawBody)
    .digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(String(signature));
  return a.length === b.length && timingSafeEqual(a, b);
}

Not every change is sent: see Sending and retries. There is one URL for all stores, and the delivery.store_id field in the body says which store a delivery belongs to. The cooperative creates the webhook secret, the key that signs each request, and sends it to the POS through a secure channel.

Event format

  • Content-Type

    Always application/json

  • User-Agent

    GenelKurye-Webhook/1

  • X-GK-Event

    The event, for example delivery.picked_up

  • X-GK-Event-Id

    The event ID, for example evt_b1tqwff2g8n4kpg36kbhhmcf_picked_up_17

  • X-GK-Timestamp

    When the request was signed, in Unix seconds, for example 1792224770

  • X-GK-Signature

    The signature, for example sha256=5d41402abc4b2a76…

X-GK-Event and X-GK-Event-Id are not signed. Use type and event_id from the body. The two headers repeat them for routing and logs.

The body:

The example is shortened: delivery is always a full delivery object, as it was when the event was sent.

Events: delivery.assigned, delivery.picked_up, delivery.in_transit, delivery.delivered, delivery.cancelled, delivery.returning, delivery.returned. There is no event for platform.

Event ID. evt_<delivery ID>_<status>, with _<courier ID> added at the end when the delivery has a courier. The courier ID is an internal number that is not in the delivery object, so treat the whole event ID as one opaque string. Every attempt to send the same event uses the same event ID. If the cooperative moves a delivery to another courier, the status does not change, but the same status is sent once more with the new courier's details and a new event ID.

Event time. occurred_at is when the delivery reached that status. For in_transit and returning it is the time of the delivery's last update, because the delivery object has no time field for them. When a status is sent again after a courier change, occurred_at is the same as in the first event, so tell such events apart by event_id.

Verifying the signature

Genel Kurye signs every request with the webhook secret. To check a request, compute the value below and compare it with X-GK-Signature:

signature = "sha256=" + hex(HMAC_SHA256(webhook_secret, X-GK-Timestamp + "." + raw_body))
  • Compute it over the bytes you received, before you parse the body.
  • Compare in constant time.
  • Reject the request if X-GK-Timestamp is more than 5 minutes away from your clock.

Sending and retries

  • Respond with a 2xx within 20 seconds. Only the status code counts. If you pass the event on before answering, answer first: a late answer is a failure and the event comes again 30 seconds later. Redirects (3xx) are not followed and count as a failure.
  • Events are sent in rounds, about every 20 seconds. If a delivery changes status more than once between two rounds, only the last status is sent. For example, if a delivery becomes picked_up and then in_transit right away, only delivery.in_transit arrives. The times of the skipped steps are in the delivery object.
  • A response other than 2xx, a timeout and a connection error are all failures. A failed event is sent again after 30 seconds, then after a wait that doubles each time, up to 30 minutes. There are 14 attempts in total (about 4 hours). After the last one the event is dropped.
  • After a 5xx, a 408, a 429, a timeout, a redirect or a connection error, no other event is sent to your URL in that round. The rest are sent in the next round.
  • Every attempt is signed again with a new X-GK-Timestamp. The event_id does not change.
  • The same event can arrive more than once. Ignore an event_id you have already processed.
  • Events are sent only for deliveries opened in the last 24 hours, and not after the restaurant removes the connection.
  • You can miss an event: when all 14 attempts fail, when a newer status replaces it in the same round, and when the delivery is older than 24 hours. To get the current status, read GET /deliveries/{id} while the connection is active.

Reference

Personal data

How a customer's and a courier's data is stored, and who can see it.

  • Until the cooperative has signed the customer data agreement with a restaurant (KVKK is Turkey's personal data protection law) and approved it for order transfer, the restaurant's orders are rejected with 403 not_approved. Nothing about a rejected order's customer is stored.
  • A platform order needs no customer data: do not send customer, the address line or note. If you send them, they are ignored and not stored. Genel Kurye keeps only the district and city of the address.
  • While a delivery is in the pool, couriers see only the customer's drop-off address. The pool card also shows where the order came from, its code, the payment method, the preparation time, the package count and the item names.
  • After a courier accepts the delivery, that courier also sees the customer's name as "Ayşe Y.".
  • After that courier picks up the package, they also see the phone number, the customer's note, the address directions and the amount to collect at the door. No other courier sees them.
  • Put anything personal about the customer, such as an allergy or a door code, in note, not in an item name. Item names are visible in the pool.
  • Every delivery object and every webhook event carries the courier's name and phone number. The number is not masked. It is there so the restaurant can reach the courier about that delivery.
  • Show the courier's name and number only to the restaurant staff who handle that delivery. Do not use them for anything else. Do not keep them after the delivery is over.
  • For a test store, Genel Kurye keeps a copy of up to 10 requests a minute, and of every webhook event for the store's deliveries, for 7 days to help with debugging. A request's copy has the path and query, the first 16 KB of the request body, the first 8 KB of the response body, the IP address and the User-Agent header. An event's copy has the event body and the first 1 KB of your response body. In these copies, the courier's phone number, the pickup code and any key are masked. The cooperative can look these copies up for you.
  • For a request to a real store, an order sent to a test store without "test": true, and an order rejected before its content is checked (401, any 403, 429 rate_limited, or 400 invalid_request on the test field), Genel Kurye keeps the names and types of the fields you sent. Of their values it keeps only those that say nothing about a person, and only when the store's keys are valid: external_order_id, platform, platform_order_code, ordered_at, prep_minutes, package_count, delivery_by, test, payment.method, payment.amount_to_collect, the items' names and quantities, and the status and occurred_at of a platform status report. A value that is not valid for its field is kept as a marker, not as sent. A top-level field that is not in this reference is kept only when its value is true or false, a small whole number or a short lowercase code. Nothing under customer or address, and never note, reason or brand. At most 2 KB per request, for 7 days. A test order rejected for its content, for example with 400 invalid_request or 422 address_too_far, is kept in full.

Reference

Versioning

This document describes v1.

New fields can be added to responses and webhook events without notice, so ignore fields you do not recognize. A change that removes a field or changes its meaning will come with a new path, /api/pos/v2.

Questions?

For a test store, a partner key or a webhook URL, or with any question about the API, write to the cooperative.

iletisim@genelkuryerkooperatifi.org