API documentation

Integrate the SAMO-TRANS exchange into your TMS or CRM

API documentation

Idempotency

The Idempotency-Key header: safe write retries without duplicates.

Networks are unreliable: a response can be lost after the server has already processed the request. Simply repeating the POST would create a duplicate listing. Send an Idempotency-Key header to prevent that.

curl -X POST "https://api.samo-trans.com/api/public/v1/proposals/cargo" \
  -H "Authorization: Bearer $SAMO_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-88213-attempt-1" \
  -d '{ "startLocations": [ … ], "endLocations": [ … ], … }'

The header is optional and applies only to writes (POST, PATCH, DELETE). It is ignored on GET, which is safe to repeat anyway.

Choosing a key

The key must be stable for one logical operation and different across operations. The most reliable source is an identifier from your own system:

Idempotency-Key: tms-order-88213

Do not generate a random string at retry time — the retry would then look like a brand new operation and the header would achieve nothing. Generate the key once, when you decide to perform the operation, and reuse it for every attempt.

Behaviour

SituationResult
First request with the keyRuns normally; the response is remembered for 24 hours
Retry: same key, same bodyThe stored response is returned. No second listing is created
Retry: same key, different body422 with code validation_error — that key already belongs to another operation
Retry while the first request is still running409 with code conflict — try again in a moment

The in-flight protection window is 60 seconds. If the first request finished sooner, the next one immediately receives the stored response.

The key is scoped to one API key, method and path: POST /proposals/cargo and POST /proposals/transport with the same Idempotency-Key are two different operations.

A retry loop worth copying

async function createWithRetry(payload, idempotencyKey) {
  for (let attempt = 0; attempt < 5; attempt += 1) {
    const res = await fetch(`${API_BASE}/api/public/v1/proposals/cargo`, {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${process.env.SAMO_KEY}`,
        "Content-Type": "application/json",
        // the same key on every attempt — that is the whole point
        "Idempotency-Key": idempotencyKey,
      },
      body: JSON.stringify(payload),
    });

    if (res.ok) return res.json();

    const { error } = await res.json();
    // 409 — a previous attempt is still in flight; 429 — rate limited. Both are worth retrying.
    if (error.code === "conflict" || error.code === "rate_limited") {
      const retryAfter = Number(res.headers.get("Retry-After") ?? 0);
      await sleep(retryAfter * 1000 || 2 ** attempt * 500);
      continue;
    }
    throw new Error(`${error.code}: ${error.message} (requestId ${error.requestId})`);
  }
  throw new Error("giving up after 5 attempts");
}

Together with batch creation

POST /proposals/batch supports Idempotency-Key too, and that is where it matters most: a retry after a dropped connection will not create 25 duplicates — it returns the stored result of the first attempt along with the outcome of each item.