cli.gs
Sign in

Report conversions

The server-to-server postback, its fields and answers.

The server postback is how your shop tells cli.gs about a sale. On this page you'll learn which request to send, which fields it takes, and how to read the answer. Your server sends one HTTPS request per order, with the ref code it saved and the order details. This is the only channel that books a commission on its own, so every integration needs it.

In short:

  • Send POST https://cli.gs/api/v1/track/conversion from your server with your API key.
  • Always send ref, orderId and the net amount. Add currency and customerType where you can.
  • Reports are idempotent: the same orderId counts once, so retries are safe.
  • 201 means recorded, 200 means already on file. Retry on 429 and 5xx, fix the request on 400, 401 and 422.

The request

text
POST https://cli.gs/api/v1/track/conversion
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

Send it from your server, never from the browser. The best moment is when the order is confirmed or paid. If the request fails with a network error, a timeout, HTTP 5xx or 429, retry it later. Repeating a report is harmless.

Parameters and code examples on the integration page
The Parameters section with ready-made code for your programme

Fields

FieldRequiredMeaning
refyesThe code from ?ref= on the landing page
orderIdyesYour order id, up to 190 characters. It makes repeated reports harmless
amountfor SALENet subtotal after discount, without tax and shipping. basketValue is accepted too
currencyrecommendedThree-letter code. Programmes settle in EUR; if omitted, the programme's currency is used
eventTypenoSALE (default), LEAD or SIGNUP
customerTypenoNEW or RETURNING, for new-customer commission rules
matchKeynoProduct or category key for rules with a product key. commissionGroup is accepted too
occurredAtnoISO 8601 time of the sale with time zone, e.g. 2026-09-28T14:05:00Z
testnotrue records a test conversion that moves no money

Details worth knowing

  • amount can be a string or a number. Use a dot as decimal separator ("149.99"). We also read 149,99 and 1.234,56. We refuse anything ambiguous such as 1.234, negative values and implausibly large baskets. For LEAD and SIGNUP the amount is optional.
  • currency other than EUR is recorded but parked for review, because programmes settle in EUR only.
  • customerType with any other value is ignored. It's not treated as RETURNING, so a typo can't lower a publisher's new-customer commission.
  • occurredAt matters when you report late, for example after payment clears. The cookie window is checked against this time, not against the time the request arrives. It must not lie before the click or in the future. Five minutes of clock difference are tolerated.
  • test must be the JSON boolean true, not 1 or "true".

Idempotency

Each order counts once. If we receive the same orderId again for the same eventType, nothing new is created. The answer is 200 with "duplicate": true and the conversion already on file. So you can safely retry after a timeout, or send the report from two places.

A lead and a sale may share an order id, for example a sign-up now and a purchase later. They're counted separately per eventType.

Examples

cURL

bash
curl -X POST https://cli.gs/api/v1/track/conversion \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "ref": "01J9Z4K6V0QX8M2N3P5R7S9T1WABCD",
    "orderId": "100234",
    "amount": "149.99",
    "currency": "EUR",
    "customerType": "NEW"
  }'

Node.js (fetch)

js
// Node 18+. Call this from your order-paid handler.
async function reportConversion(order) {
  const res = await fetch("https://cli.gs/api/v1/track/conversion", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.CLIGS_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      ref: order.cligsRef,              // saved with the order at checkout
      orderId: String(order.id),
      amount: order.netSubtotal.toFixed(2),
      currency: "EUR",
      customerType: order.isFirstOrder ? "NEW" : "RETURNING",
      occurredAt: order.createdAt.toISOString(),
    }),
    signal: AbortSignal.timeout(5000),
  });

  if (res.ok) return res.json();                           // 200 or 201
  if (res.status >= 500 || res.status === 429) throw new Error("retry later");
  const err = await res.json().catch(() => ({}));
  console.error("cli.gs refused the conversion", res.status, err.error); // do not retry unchanged
}

PHP (curl)

php
<?php
function cligs_report_conversion(array $data): array {
    $ch = curl_init('https://cli.gs/api/v1/track/conversion');
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 5,
        CURLOPT_HTTPHEADER     => [
            'Authorization: Bearer ' . getenv('CLIGS_API_KEY'),
            'Content-Type: application/json',
        ],
        CURLOPT_POSTFIELDS     => json_encode($data),
    ]);
    $body   = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_close($ch);
    return ['status' => $status, 'body' => json_decode((string) $body, true)];
}

$result = cligs_report_conversion([
    'ref'      => $order['cligs_ref'],
    'orderId'  => (string) $order['id'],
    'amount'   => number_format($order['net_subtotal'], 2, '.', ''),
    'currency' => 'EUR',
]);

WooCommerce

This example saves the ref code on the order at checkout. It reports the sale when the order moves to Processing, which means paid. It assumes the cligs_ref cookie from Capture the ref code.

php
<?php
// 1. At checkout: copy the ref code from the cookie onto the order.
function cligs_save_ref_on_order($order) {
    $ref = $_COOKIE['cligs_ref'] ?? '';
    if ($ref !== '' && preg_match('/^[0-9A-Za-z]{10,64}$/', $ref)) {
        $order->update_meta_data('_cligs_ref', $ref);
    }
}
add_action('woocommerce_checkout_create_order', 'cligs_save_ref_on_order'); // classic checkout
add_action('woocommerce_store_api_checkout_update_order_from_request', function ($order) { // block checkout
    cligs_save_ref_on_order($order);
    $order->save();
});

// 2. When the order is paid: report it. Runs server-side, also for background payment callbacks.
add_action('woocommerce_order_status_processing', function ($order_id) {
    $order = wc_get_order($order_id);
    $ref   = $order ? $order->get_meta('_cligs_ref') : '';
    if (!$ref || $order->get_meta('_cligs_reported')) {
        return;
    }

    // Net subtotal after discount, excluding tax and shipping.
    $amount = (float) $order->get_subtotal() - (float) $order->get_discount_total();

    $response = wp_remote_post('https://cli.gs/api/v1/track/conversion', [
        'headers' => [
            'Authorization' => 'Bearer ' . CLIGS_API_KEY, // define in wp-config.php
            'Content-Type'  => 'application/json',
        ],
        'body'    => wp_json_encode([
            'ref'        => $ref,
            'orderId'    => (string) $order->get_id(),
            'amount'     => number_format(max(0, $amount), 2, '.', ''),
            'currency'   => $order->get_currency(),
            'occurredAt' => $order->get_date_created()->format(DATE_ATOM),
        ]),
        'timeout' => 5,
    ]);

    $code = wp_remote_retrieve_response_code($response);
    if ($code === 200 || $code === 201) {
        $order->update_meta_data('_cligs_reported', 1);
        $order->save();
    }
});

The _cligs_reported flag only saves a request; the report itself is idempotent. If a request fails, you can simply report the order again later. A scheduled job that looks for paid orders with _cligs_ref but without _cligs_reported does that well.

Responses

A successful report returns the conversion as we recorded it:

json
{
  "ok": true,
  "conversionId": "cmg3x1q2w0001abcd9876efgh",
  "status": "PENDING",
  "commission": 12,
  "duplicate": false,
  "confirmed": false,
  "test": false
}

What the fields mean

  • status is PENDING when the commission is booked and waiting for the approval window. It's NEEDS_REVIEW when the conversion is parked for you to decide, for example because of a non-EUR currency, no matching commission rule, not enough credit, or fraud signals. It's REJECTED for a test or for a publisher who is no longer active.
  • commission is the publisher's commission in the programme currency.
  • duplicate: true means this order was already on file. Nothing changed.
  • confirmed: true means this report confirmed a conversion the tracking pixel had already reported for the same order.

HTTP status codes

HTTPBodyWhat to do
201ok: trueRecorded. Done
200ok: true, duplicate or confirmedAlready on file, or pixel conversion confirmed. Done
400error: "invalid payload" with issues, or "invalid json body"Fix the request (missing orderId, wrong eventType, test not a boolean, ...)
401e.g. "invalid api key", "invalid signature"Check the key and the signature
403error: "REF_PROGRAM_MISMATCH"The ref code belongs to another programme; use that programme's key
422error: "<CODE>"The report was understood but not accepted. Retrying unchanged won't help
429"rate limit exceeded"More than 600 requests per minute for this key. Retry later
5xxTemporary problem on our side. Retry later

The 422 codes (REF_UNKNOWN, REF_EXPIRED, INVALID_BASKET and others) are explained with their fixes in Testing and diagnostics.

Next steps

Protect the request with a signature, then handle returns and reversals.