Kawenter · Travel agents

Partner API

Server-to-server API for approved travel agents. Authenticate with a ka_live_… API key, quote agent-priced eVisas, create orders, and pay from your agency wallet. Fees and discounts are always computed by Kawenter — never send totals as authoritative input.

Discovery

Machine-readable resources for search engines, LLM tools, and integrators:

Your API keys

Loading API keys…

Quick start

  1. Generate an API key above (approved travel agents only).
  2. Top up your agency wallet in the Kawenter website or app.
  3. Call the catalog → quote → create order → pay-wallet flow from your server.
  4. Poll GET /orders/{id} until the order is complete.
Server-to-server only. Keep your ka_live_… key on your backend. Do not embed it in mobile apps or browser JavaScript.
curl · smoke test
export KAWENTER_API_KEY="ka_live_…"
export KAWENTER_API="https://kawenter.com/api/partner/v1"

curl -sS "$KAWENTER_API" \
  -H "X-Api-Key: $KAWENTER_API_KEY" | jq .

curl -sS "$KAWENTER_API/wallet" \
  -H "X-Api-Key: $KAWENTER_API_KEY" | jq .

Authentication

Send your key on every request using either header:

Headers
X-Api-Key: ka_live_…
# or
Authorization: Bearer ka_live_…
  • Base URL: https://kawenter.com/api/partner/v1
  • JSON only — Content-Type: application/json on POST
  • All amounts are USD

Errors

Errors use a consistent JSON shape with machine-readable details so your integration knows exactly what broke (missing fields, scope, wallet shortfall, file limits, etc.). Every response also sends X-Request-Id — include it when contacting support.

Error response
{
  "ok": false,
  "error_code": "INSUFFICIENT_WALLET",
  "message": "Insufficient wallet balance.",
  "status": 402,
  "retry_after": null,
  "request_id": "prt_a1b2c3d4e5f60718",
  "method": "POST",
  "path": "orders/123/pay-wallet",
  "docs_url": "https://kawenter.com/docs/partner#errors",
  "details": {
    "balance_cents": 1200,
    "required_cents": 5000,
    "shortfall_cents": 3800,
    "currency": "USD",
    "hint": "Top up the agency wallet, then retry pay-wallet with the same Idempotency-Key.",
    "action": "topup_wallet"
  }
}
HTTPTypical error_codeMeaning
401AUTH_REQUIRED / INVALID_API_KEYMissing or bad API key
403INSUFFICIENT_SCOPE / API_KEY_REVOKED / API_KEY_EXPIREDScope missing, key revoked, or expired — see details
402INSUFFICIENT_WALLETWallet too low — details.balance_cents / required_cents
404ORDER_NOT_FOUND / EVISA_NOT_FOUND / DOCUMENT_NOT_READYResource missing or not ready yet
413FILE_TOO_LARGEUpload over 15 MB — see details.max_bytes
429RATE_LIMIT_EXCEEDEDKey or agency quota — see Retry-After + limit_scope
502/503UPSTREAM_ERROR / SERVICE_UNAVAILABLETemporary backend issue — retry with backoff

Idempotency

POST /orders/evisa requires an Idempotency-Key header (or idempotency_key / client_reference in the body). Retries with the same key return the original result — safe for network failures. Recommended on pay-wallet as well.

Endpoints

Full workflow for integrating eVisa sales into your app or back office.

GET /api/partner/v1

Health check and agency identity for this API key.

Example response 200

JSON
{
  "ok": true,
  "api": "partner",
  "version": "v1",
  "agency_uid": "abc123…",
  "discount_percent": 15,
  "scopes": ["evisa", "wallet", "files", "webhooks"],
  "rate_limit_per_minute": 30,
  "rate_limit_per_day": 1000,
  "docs_url": "https://kawenter.com/docs/partner",
  "openapi_url": "https://kawenter.com/.well-known/partner-openapi.json"
}
GET /api/partner/v1/wallet

Current agency wallet balance (USD). Partner API can only spend — top up via Kawenter web/app. Scope: wallet.

curl
curl -sS "https://kawenter.com/api/partner/v1/wallet" \
  -H "X-Api-Key: $KAWENTER_API_KEY"

Example response 200

JSON
{
  "ok": true,
  "agency_uid": "abc123…",
  "balance_cents": 125000,
  "balance": 1250.00,
  "currency": "USD"
}
GET /api/partner/v1/wallet/transactions

Wallet ledger (debits, top-ups, reversals, refunds) for reconciliation. Scope: wallet.

QueryRequiredDescription
limitNo1–50 (default 25)
cursorNocreated_at from previous page next_cursor
curl
curl -sS "https://kawenter.com/api/partner/v1/wallet/transactions?limit=25" \
  -H "X-Api-Key: $KAWENTER_API_KEY"

Example response 200

JSON
{
  "ok": true,
  "agency_uid": "abc123…",
  "items": [
    {
      "operation_id": "…",
      "delta_cents": -6700,
      "balance_after": 118300,
      "reason": "wallet_pay_order",
      "reference_id": "153800",
      "created_at": "2026-10-20T12:00:00Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
POST /api/partner/v1/files

Upload a passport or photo securely. Returns a file_id (no public URL). Use that id on create-order. Max 15 MB; JPG/PNG/PDF/WEBP/HEIC/TIFF. Scope: files.

curl
curl -sS -X POST "https://kawenter.com/api/partner/v1/files" \
  -H "X-Api-Key: $KAWENTER_API_KEY" \
  -F "file=@/path/passport.jpg" \
  -F "purpose=passport"

Example response 201

JSON
{
  "ok": true,
  "file_id": "pf_a1b2c3…",
  "content_type": "image/jpeg",
  "size": 245760,
  "purpose": "passport"
}
GET /api/partner/v1/catalog/evisas

List eVisas for a passport nationality with agent unit fees already applied.

QueryRequiredDescription
nationalityYesISO country code, e.g. IQ
destinationNoFilter destination, e.g. TR
langNoen (default) or ar
curl
curl -sS "https://kawenter.com/api/partner/v1/catalog/evisas?nationality=IQ&lang=en" \
  -H "X-Api-Key: $KAWENTER_API_KEY"

Example response 200

JSON
{
  "ok": true,
  "nationality": "IQ",
  "destination": "",
  "count": 1,
  "evisas": [
    {
      "id": "123",
      "title": "Turkey eVisa",
      "unit_government_fee": 50,
      "unit_processing_fee_base": 20,
      "unit_processing_fee": 17,
      "agency_discount_percent": 15,
      "image": "https://…/turkey.webp",
      "destination_code": "TR"
    }
  ]
}

Detail: GET /catalog/evisas/{id}?nationality=IQ

POST /api/partner/v1/quotes/evisa

Server-priced quote for one or more applicants, plus full eVisa details (requirements, important notes, processing time, add-ons). Fee math is server-side only — never send addon_fee, insurance_fee, or totals. Pass addon_id (from evisa.addons[].id) for catalog add-on prices × applicants. Pass include_insurance: true to add catalog insurance once per order (not × applicants). Create-order requires an add-on when the catalog lists any.

curl
curl -sS -X POST "https://kawenter.com/api/partner/v1/quotes/evisa" \
  -H "X-Api-Key: $KAWENTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "evisa_id": "73",
    "nationality": "EG",
    "applicant_count": 3,
    "addon_id": "addon_0",
    "include_insurance": true
  }'

Example response 200

JSON
{
  "ok": true,
  "quote": {
    "evisa_id": "73",
    "title": "Afghanistan evisa",
    "nationality": "EG",
    "destination": "AF",
    "applicant_count": 3,
    "government_fee": 447,
    "processing_fee_base": 138,
    "processing_fee": 110.4,
    "unit_addon_fee": 0,
    "addon_fee": 0,
    "selected_addons": [
      {
        "id": "addon_0",
        "title": "Validity up to 30 Days",
        "unit_price": 0,
        "price": 0,
        "currency": "USD"
      }
    ],
    "insurance_included": true,
    "unit_insurance_fee": 9,
    "insurance_fee": 9,
    "insurance_refund_amount": 149,
    "agency_discount_percent": 20,
    "agency_discount_amount": 27.6,
    "total": 566.4,
    "currency": "USD",
    "visa_type": "eVisa",
    "validity": "30",
    "duration_of_stay": "30",
    "number_of_entry": "Single entry",
    "processing_time": "5-7",
    "processing_time_label": "5-7 working days",
    "requirements": [
      "Passport (6 months validity)",
      "Copy of Recent Photo"
    ],
    "important_notes": [
      "Fees are non-refundable in case of Visa refusal",
      "This visa is for tourism purposes only"
    ],
    "evisa": {
      "id": "73",
      "title": "Afghanistan evisa",
      "destination": "AF",
      "requirements": ["Passport (6 months validity)", "Copy of Recent Photo"],
      "important_notes": ["Fees are non-refundable in case of Visa refusal"],
      "addons": [
        {
          "id": "addon_0",
          "title": "Validity up to 30 Days",
          "price": 0,
          "currency": "USD"
        }
      ],
      "insurance": {
        "available": true,
        "fee": 9,
        "unit_fee": 9,
        "refund_amount": 149,
        "currency": "USD"
      }
    }
  }
}
POST /api/partner/v1/orders/evisa

Create an unpaid eVisa order with the same data as the website new-application page. Fees (government, processing, add-ons, insurance) are recomputed server-side — never send fee amounts. Check quote.required_fields after quoting (job / mother / travel dates vary by catalog). Prefer passport_file_id / photo_file_id from POST /files. Kawenter processes these orders like website applications (documents, tracking, status webhooks).

Header / fieldRequiredNotes
Idempotency-KeyYesUnique per create attempt (max 128 chars)
evisa_id, nationalityYes
addon_id / addonsWhen catalog has add-onsUse evisa.addons[].id. Price from catalog × applicants.
include_insuranceNoCatalog insurance once per order when available.
contact.email, contact.phoneYesPhone with country code (e.g. +9647…)
entry_date, return_dateUsuallyYYYY-MM-DD unless catalog hides travel dates
applicants[].full_nameYes
applicants[].passport_file_idYesOr https passport_url
applicants[].photo_file_idYesOr https photo_url (same as website)
applicants[].job, mother_namePer catalogSee required_fields on the quote
client_referenceNoYour CRM / booking id (stored on the order)
curl
curl -sS -X POST "https://kawenter.com/api/partner/v1/orders/evisa" \
  -H "X-Api-Key: $KAWENTER_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-2026-001" \
  -d '{
    "evisa_id": "73",
    "nationality": "EG",
    "addon_id": "addon_0",
    "include_insurance": true,
    "entry_date": "2026-11-01",
    "return_date": "2026-11-15",
    "contact": {
      "email": "[email protected]",
      "phone": "+2010xxxxxxx",
      "name": "Ali Hassan"
    },
    "applicant_count": 2,
    "applicants": [
      {
        "full_name": "Ali Hassan",
        "job": "Engineer",
        "mother_name": "Fatima",
        "passport_file_id": "pf_a1b2c3…",
        "photo_file_id": "pf_d4e5f6…"
      },
      {
        "full_name": "Sara Hassan",
        "job": "Teacher",
        "mother_name": "Fatima",
        "passport_file_id": "pf_g7h8i9…",
        "photo_file_id": "pf_j0k1l2…"
      }
    ],
    "client_reference": "your-crm-id-001"
  }'

Send one applicants[] entry per traveler (up to 20). Fees are multiplied by traveler count; WooCommerce receives newline-joined passport_url, photo_url, work, and mother metas — same pattern as the website multi-traveler checkout.

Example response 201

JSON
{
  "ok": true,
  "order_id": "153800",
  "order": {
    "order_id": "153800",
    "number": "153800",
    "status": "pending",
    "currency": "USD",
    "total": "67.00",
    "needs_payment": true,
    "billing": {
      "name": "Ali Hassan",
      "email": "[email protected]",
      "phone": "+9647…",
      "country": "IQ"
    },
    "meta": {
      "order_type": "evisa",
      "agency_discount_percent": "15"
    }
  },
  "quote": {
    "government_fee": 50,
    "processing_fee": 17,
    "total": 67,
    "currency": "USD"
  },
  "idempotency_key": "order-2026-001"
}
POST /api/partner/v1/orders/{id}/pay-wallet

Debit the agency wallet and mark the order paid. Safe to retry with the same idempotency key.

curl
curl -sS -X POST "https://kawenter.com/api/partner/v1/orders/153800/pay-wallet" \
  -H "X-Api-Key: $KAWENTER_API_KEY" \
  -H "Idempotency-Key: pay-153800"

Example response 200

JSON
{
  "ok": true,
  "order_id": "153800",
  "order": {
    "order_id": "153800",
    "status": "processing",
    "total": "67.00",
    "needs_payment": false
  },
  "wallet": {
    "balance_cents": 118300,
    "delta_cents": -6700,
    "operation_id": "…",
    "duplicate": false
  },
  "payment_method": "wallet",
  "idempotency_key": "pay-153800"
}
GET /api/partner/v1/orders/{id}

Fetch a single order owned by your agency. Prefer webhooks over polling. When more documents are needed, WooCommerce status is on-hold or not_completed_doc (same as check-status “Documents uncompleted”): needs_documents is true, extra_docs has the staff note, and check_status_url opens the public status page.

curl
curl -sS "https://kawenter.com/api/partner/v1/orders/153800" \
  -H "X-Api-Key: $KAWENTER_API_KEY"

Example response 200

JSON
{
  "ok": true,
  "order": {
    "order_id": "153800",
    "number": "153800",
    "status": "completed",
    "currency": "USD",
    "total": "67.00",
    "needs_payment": false,
    "needs_documents": false,
    "has_document": true,
    "documents_url": "/api/partner/v1/orders/153800/documents",
    "tracking": "TRK-88421",
    "check_status_url": "https://kawenter.com/check-status?order=153800&email=customer%40example.com",
    "date_created": "2026-10-20T14:11:25",
    "billing": { "name": "Ali Hassan", "email": "[email protected]" },
    "meta": { "order_type": "evisa", "applicant_count": "1" }
  }
}

When staff set a tracking number (same field as check-status), it appears as tracking on get/list order and on webhooks.

Incomplete documents example

JSON
{
  "ok": true,
  "order": {
    "order_id": "153800",
    "number": "153800",
    "status": "not_completed_doc",
    "status_label": "Documents uncompleted",
    "needs_payment": false,
    "needs_documents": true,
    "has_document": false,
    "extra_docs": "Please re-upload a clearer passport scan (page with photo).",
    "check_status_url": "https://kawenter.com/check-status?order=153800&email=customer%40example.com",
    "billing": { "name": "Ali Hassan", "email": "[email protected]" }
  }
}
GET /api/partner/v1/orders/{id}/documents

Download the approved visa PDF (or image) with your API key — no Kawenter login. Returns binary with Content-Disposition: attachment. 404 DOCUMENT_NOT_READY until the visa is issued.

curl
curl -sS -o visa-153800.pdf \
  "https://kawenter.com/api/partner/v1/orders/153800/documents" \
  -H "X-Api-Key: $KAWENTER_API_KEY"
GET /api/partner/v1/orders

List recent partner orders. Default is a fast index (no WooCommerce round-trips). Use ?expand=1 for full live order objects (fetched in parallel). Prefer webhooks + GET /orders/{id} for status instead of polling expand lists.

QueryDefaultNotes
limit20Max 50
expandoff1 / full = live WC details
curl
curl -sS "https://kawenter.com/api/partner/v1/orders?limit=20" \
  -H "X-Api-Key: $KAWENTER_API_KEY"

Example response (fast, default) 200

JSON
{
  "ok": true,
  "count": 1,
  "expand": false,
  "orders": [
    {
      "order_id": "153800",
      "status": "processing",
      "total": "67.00",
      "title": "Turkey eVisa",
      "indexed": true
    }
  ]
}
POST /api/partner/v1/webhooks

Register an HTTPS callback so Kawenter notifies you when an order changes — no polling required (important with the 1,000/day default). Scope: webhooks.

Events: order.processing, order.completed, order.documents_required (WC on-hold / not_completed_doc — incomplete docs), order.rejected.

curl
curl -sS -X POST "https://kawenter.com/api/partner/v1/webhooks" \
  -H "X-Api-Key: $KAWENTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://your-app.example/hooks/kawenter" }'

Example response 201

JSON
{
  "ok": true,
  "webhook_id": "wh_…",
  "url": "https://your-app.example/hooks/kawenter",
  "events": ["order.processing", "order.completed", "order.documents_required", "order.rejected"],
  "signing_secret": "whsec_…",
  "message": "Copy signing_secret now — it will not be shown again."
}

Also: GET /webhooks (list), DELETE /webhooks/{id} (deactivate).

Delivery payload

JSON
{
  "event": "order.completed",
  "order_id": "153800",
  "number": "153800",
  "status": "completed",
  "previous_status": "processing",
  "updated_at": "2026-10-21T08:00:00Z",
  "has_document": true,
  "needs_documents": false,
  "check_status_url": "https://kawenter.com/check-status?order=153800&email=…",
  "agency_uid": "abc123…"
}

Incomplete documents webhook

JSON
{
  "event": "order.documents_required",
  "order_id": "153800",
  "status": "not_completed_doc",
  "status_label": "Documents uncompleted",
  "needs_documents": true,
  "extra_docs": "Please re-upload a clearer passport scan (page with photo).",
  "check_status_url": "https://kawenter.com/check-status?order=153800&email=…",
  "has_document": false
}

Signature header: X-Kawenter-Signature: t=<unix>,v1=<hmac_sha256(t + "." + body, signing_secret)> plus X-Kawenter-Timestamp and X-Kawenter-Event-Id. Reject events older than ~5 minutes and ignore duplicate event_id values (replay protection). When has_document is true, call GET …/documents. When needs_documents is true, show extra_docs or open check_status_url.

Node verify
const crypto = require("crypto");
function verifyKawenter(rawBody, signatureHeader, secret, maxSkewSec = 300) {
  const parts = Object.fromEntries(
    String(signatureHeader || "").split(",").map((p) => p.split("="))
  );
  const t = Number(parts.t || 0);
  if (!t || Math.abs(Date.now() / 1000 - t) > maxSkewSec) return false;
  const expected = crypto.createHmac("sha256", secret).update(t + "." + rawBody).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(String(parts.v1 || ""));
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

API key scopes

Each key has scopes that gate routes. Missing scope → 403 INSUFFICIENT_SCOPE. Keys can expire (expires_at) and be rotated from the keys panel above.

ScopeAllows
evisaCatalog, quote, create/list/get orders, download documents
walletBalance, transactions, pay-wallet
filesPOST /files
webhooksRegister / list / delete webhooks

Issue a catalog-only key (no wallet spend) by passing a subset of scopes when generating a key via the self-service API.

Code samples

Drop these into your backend. Replace the API key with an environment variable.

Node.js (fetch)

JavaScript
const API = "https://kawenter.com/api/partner/v1";
const KEY = process.env.KAWENTER_API_KEY;

async function kawenter(path, { method = "GET", body, idempotencyKey } = {}) {
  const headers = {
    "X-Api-Key": KEY,
    Accept: "application/json",
  };
  if (body) headers["Content-Type"] = "application/json";
  if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;

  const res = await fetch(`${API}${path}`, {
    method,
    headers,
    body: body ? JSON.stringify(body) : undefined,
  });
  const data = await res.json();
  if (!res.ok) {
    const err = new Error(data.message || res.statusText);
    err.code = data.error_code;
    err.status = res.status;
    err.retryAfter = data.retry_after;
    throw err;
  }
  return data;
}

// Example: quote → create → pay
const quote = await kawenter("/quotes/evisa", {
  method: "POST",
  body: { evisa_id: "123", nationality: "IQ", applicant_count: 1 },
});

const created = await kawenter("/orders/evisa", {
  method: "POST",
  idempotencyKey: "order-" + Date.now(),
  body: {
    evisa_id: "123",
    nationality: "IQ",
    contact: { email: "[email protected]", name: "Ali Hassan" },
    applicants: [
      {
        full_name: "Ali Hassan",
        passport_url: "https://example.com/passport.jpg",
      },
    ],
  },
});

const paid = await kawenter(`/orders/${created.order_id}/pay-wallet`, {
  method: "POST",
  idempotencyKey: "pay-" + created.order_id,
});
console.log(paid.order.status, paid.wallet);

PHP

PHP
function kawenter_partner(string $path, string $method = 'GET', ?array $body = null, ?string $idem = null): array {
    $ch = curl_init('https://kawenter.com/api/partner/v1' . $path);
    $headers = [
        'X-Api-Key: ' . getenv('KAWENTER_API_KEY'),
        'Accept: application/json',
    ];
    if ($body !== null) {
        $headers[] = 'Content-Type: application/json';
    }
    if ($idem) {
        $headers[] = 'Idempotency-Key: ' . $idem;
    }
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST => $method,
        CURLOPT_HTTPHEADER => $headers,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POSTFIELDS => $body !== null ? json_encode($body) : null,
    ]);
    $raw = curl_exec($ch);
    $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    $data = json_decode((string) $raw, true) ?: [];
    if ($code >= 400) {
        throw new RuntimeException($data['message'] ?? ('HTTP ' . $code));
    }
    return $data;
}

$wallet = kawenter_partner('/wallet');
$catalog = kawenter_partner('/catalog/evisas?nationality=IQ');

Python

Python
import os, requests

API = "https://kawenter.com/api/partner/v1"
KEY = os.environ["KAWENTER_API_KEY"]

def kawenter(path, method="GET", json=None, idempotency_key=None):
    headers = {"X-Api-Key": KEY, "Accept": "application/json"}
    if idempotency_key:
        headers["Idempotency-Key"] = idempotency_key
    r = requests.request(method, API + path, headers=headers, json=json, timeout=60)
    data = r.json()
    if r.status_code >= 400:
        raise RuntimeError(data.get("message") or r.text)
    return data

wallet = kawenter("/wallet")
quote = kawenter("/quotes/evisa", "POST", {
    "evisa_id": "123",
    "nationality": "IQ",
    "applicant_count": 1,
})

Rate limits

Limits are per API key (not per agency and not per IP). Every authenticated call counts against both windows. Quotas are also returned on GET /api/partner/v1.

Window Default Maximum (on request) Scope
Per minute (key) 30 requests 120 Rolling 60 seconds per API key
Per day (key) 1,000 requests 50,000 Rolling 24 hours per API key
Per minute (agency) 60 requests 180 Shared across all keys for the agency
Per day (agency) 2,000 requests 6,000 Shared across all keys for the agency

Response headers

  • X-RateLimit-Limit / Remaining / Reset — per-key minute window
  • X-RateLimit-Limit-Day / Remaining-Day / Reset-Day — per-key daily window
  • X-RateLimit-Limit-Agency / -AgencyDay — agency-wide shared quotas
  • Retry-After — seconds to wait (only on 429)

When a limit is exceeded

429 JSON
{
  "ok": false,
  "error_code": "RATE_LIMIT_EXCEEDED",
  "message": "Partner API per-minute rate limit exceeded for this API key. Slow down and retry after Retry-After seconds.",
  "status": 429,
  "retry_after": 42,
  "limit": 30,
  "window": 60,
  "remaining": 0,
  "reset": 1730000042,
  "limit_scope": "key",
  "window_name": "minute",
  "request_id": "prt_a1b2c3d4e5f60718",
  "docs_url": "https://kawenter.com/docs/partner#rate-limits",
  "details": {
    "limit": 30,
    "window": 60,
    "window_name": "minute",
    "remaining": 0,
    "reset": 1730000042,
    "limit_scope": "key",
    "hint": "Per-key minute quota exhausted. Back off using Retry-After.",
    "action": "retry_after"
  }
}

Tips: reuse Idempotency-Key on retries, cache catalog results, and use separate keys for production vs staging. Need a higher limit? Contact Kawenter support.

Machine-readable: OpenAPI 3.0 · Markdown · Agent JSON · llms.txt