API documentation

Integrate the SAMO-TRANS exchange into your TMS or CRM

API documentation

Examples

Ready-to-run curl for dictionaries, search, listing creation, batches and sync.

All examples assume two environment variables:

export SAMO_API="https://api.samo-trans.com/api/public/v1"
export SAMO_KEY="samo_live_XXXXXXXXXXXXXXXXXXXXXXXX"

Dictionaries

Before creating your first listing, fetch the dictionaries — these are the codes the API accepts. They change rarely, so cache them on your side.

curl "$SAMO_API/reference/car-types"     -H "Authorization: Bearer $SAMO_KEY"
curl "$SAMO_API/reference/load-types"    -H "Authorization: Bearer $SAMO_KEY"
curl "$SAMO_API/reference/permits"       -H "Authorization: Bearer $SAMO_KEY"
curl "$SAMO_API/reference/currencies"    -H "Authorization: Bearer $SAMO_KEY"
curl "$SAMO_API/reference/payment-types" -H "Authorization: Bearer $SAMO_KEY"

Each entry carries a code and labels in five languages:

{ "code": "tent", "labels": { "uk": "Тент", "en": "Tent", "ru": "Тент", "de": "Plane", "pl": "Plandeka" } }

Geography:

curl "$SAMO_API/reference/countries?locale=en" -H "Authorization: Bearer $SAMO_KEY"
curl "$SAMO_API/reference/regions?country=UA&locale=en" -H "Authorization: Bearer $SAMO_KEY"

# locality search → this is where route osmIds come from
curl "$SAMO_API/localities/search?q=Kovel&country=UA&locale=en" \
  -H "Authorization: Bearer $SAMO_KEY"

Searching the exchange

curl -G "$SAMO_API/proposals/search" \
  -H "Authorization: Bearer $SAMO_KEY" \
  --data-urlencode "type=CARGO" \
  --data-urlencode "startCountries=UA" \
  --data-urlencode "endCountries=PL" \
  --data-urlencode "minCapacity=15" \
  --data-urlencode "dateFrom=2026-08-10" \
  --data-urlencode "limit=50"

Geo filters are arrays, so repeat the parameter:

curl -G "$SAMO_API/proposals/search" \
  -H "Authorization: Bearer $SAMO_KEY" \
  --data-urlencode "startRegions=UA-07" \
  --data-urlencode "startRegions=UA-05" \
  --data-urlencode "carTypes=tent" \
  --data-urlencode "carTypes=ref"

A single listing:

curl "$SAMO_API/proposals/clx7k2n9p0001/?locale=en" -H "Authorization: Bearer $SAMO_KEY"

Contacts appear in the response only if your plan allows seeing them — otherwise "contact": null.

Creating a listing

Cargo:

curl -X POST "$SAMO_API/proposals/cargo" \
  -H "Authorization: Bearer $SAMO_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: tms-order-88213" \
  -d '{
    "startLocations": [{ "type": "locality", "osmId": "3678531", "countryCode": "UA" }],
    "endLocations":   [{ "type": "country",  "countryCode": "PL" }],
    "dataLoadStart": "2026-08-12T00:00:00.000Z",
    "dataLoadEnd":   "2026-08-14T00:00:00.000Z",
    "capacity": 20,
    "volume": 86,
    "description": "Pallets, 20 t, side curtain access",
    "carTypes": ["tent"],
    "loadUnloadTypes": ["side", "back"],
    "carPermits": ["cmr", "t1"],
    "price": 24500,
    "currencyType": "UAH",
    "paymentType": "CASHLESS"
  }'

Transport uses the same body shape and a different path:

curl -X POST "$SAMO_API/proposals/transport" \
  -H "Authorization: Bearer $SAMO_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "startLocations": [ … ], "endLocations": [ … ], "dataLoadStart": "…", "dataLoadEnd": "…", "capacity": 22, "description": "Tilt 92 m³, free from Monday", "carTypes": ["tent"] }'

Required fields: startLocations, endLocations, dataLoadStart, dataLoadEnd, capacity, description, carTypes.

Do not send companyId — it always comes from the key; companyId in the body is not ignored, it fails the request with 400 validation_error. The contact person (contactPersonId) is optional — omit it and the contact becomes whoever issued the key. See "Assigning a listing to an employee" below for details.

Batch creation

Up to 25 listings in one request. The response is always 200, with a per-item result.

curl -X POST "$SAMO_API/proposals/batch" \
  -H "Authorization: Bearer $SAMO_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: tms-batch-2026-08-05-01" \
  -d '{
    "items": [
      { "type": "CARGO",     "startLocations": [ … ], "endLocations": [ … ], "…": "…" },
      { "type": "TRANSPORT", "startLocations": [ … ], "endLocations": [ … ], "…": "…" }
    ]
  }'
{
  "created": 1,
  "failed": 1,
  "results": [
    { "index": 0, "status": "created", "proposal": { "id": "clx…", "…": "…" } },
    { "index": 1, "status": "error", "error": { "code": "validation_error", "message": "capacity must be a number" } }
  ]
}

A batch consumes as many units of the daily quota as it has items. If the quota would not cover it, the whole request is rejected with 429 quota_exceeded and nothing is created.

Your own listings

# list (only the company the key belongs to)
curl -G "$SAMO_API/proposals/my" \
  -H "Authorization: Bearer $SAMO_KEY" \
  --data-urlencode "status=ACTIVE" \
  --data-urlencode "limit=100"

# partial update — send only the fields that changed
curl -X PATCH "$SAMO_API/proposals/clx7k2n9p0001" \
  -H "Authorization: Bearer $SAMO_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "price": 26000, "description": "Updated: part loads possible" }'

# push back to the top of search
curl -X POST "$SAMO_API/proposals/clx7k2n9p0001/bump" -H "Authorization: Bearer $SAMO_KEY"

# take off air / put back
curl -X POST "$SAMO_API/proposals/clx7k2n9p0001/archive" -H "Authorization: Bearer $SAMO_KEY"
curl -X POST "$SAMO_API/proposals/clx7k2n9p0001/restore" -H "Authorization: Bearer $SAMO_KEY"

# delete permanently
curl -X DELETE "$SAMO_API/proposals/clx7k2n9p0001" -H "Authorization: Bearer $SAMO_KEY"

Worth knowing about these actions:

  • bump is available to the listing's author or its contact person. Being the company owner is not enough on its own. Bumping too soon or exhausting the bump limit returns 400 validation_error with the reason in message.
  • archive and restore are idempotent: calling either on a listing already in that state returns 200 and changes nothing.
  • restore is not free: putting a listing back on air resets its createdAt and view counter, and it occupies an active-listing slot on your plan. At the cap you get a 400.
  • Touching another company's listing returns 404, not 403.

Employees

# list the staff of the company the key belongs to
curl "$SAMO_API/employees" -H "Authorization: Bearer $SAMO_KEY"

# how many listings a specific employee is on — worth checking before deactivating them
curl "$SAMO_API/employees/clx9f4k7r0004/proposals-count" -H "Authorization: Bearer $SAMO_KEY"

# temporarily block access
curl -X PATCH "$SAMO_API/employees/clx9f4k7r0004/status" \
  -H "Authorization: Bearer $SAMO_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "status": "DEACTIVATED" }'

# restore access
curl -X PATCH "$SAMO_API/employees/clx9f4k7r0004/status" \
  -H "Authorization: Bearer $SAMO_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "status": "ACTIVE" }'

# change role
curl -X PATCH "$SAMO_API/employees/clx9f4k7r0004" \
  -H "Authorization: Bearer $SAMO_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "role": "DISPATCHER" }'

List response:

[
  {
    "id": "clx9f4k7r0004",
    "userId": "clx2h6m3t0005",
    "email": "[email protected]",
    "name": "Оксана Ковальчук",
    "role": "DISPATCHER",
    "status": "ACTIVE",
    "invitedAt": "2026-01-02T10:00:00.000Z",
    "joinedAt": "2026-01-03T09:30:00.000Z",
    "avatar": null,
    "phones": [{ "phone": "+380501234567", "isPrimary": true, "hasTelegram": true, "hasWhatsApp": false, "hasViber": false }]
  }
]

Note the two different identifiers: id is the membership and goes in the path of these endpoints; userId is the person, and that is what a listing's contactPersonId takes.

Deactivating someone does not unpublish their listings — those stay on air and keep showing them as the contact. Check proposals-count first and reassign with PATCH /proposals/{id} if needed.

Creating and deleting employees is not available through the API — that is done in the dashboard.

Assigning a listing to an employee

curl -X POST "$SAMO_API/proposals/cargo" \
  -H "Authorization: Bearer $SAMO_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "startLocations": [{ "type": "country", "countryCode": "UA" }],
    "endLocations":   [{ "type": "country", "countryCode": "PL" }],
    "dataLoadStart": "2026-08-12T00:00:00.000Z",
    "dataLoadEnd":   "2026-08-14T00:00:00.000Z",
    "capacity": 20,
    "description": "Pallets",
    "carTypes": ["tent"],
    "contactPersonId": "clx2h6m3t0005"
  }'

contactPersonId is the userId from the employee list. Only the company owner and employees with status ACTIVE are accepted; anyone else fails with 400 validation_error. Omit the field and the contact becomes whoever issued the key.

Counterparty lookup

curl "$SAMO_API/companies/clx4a1b2c0001" -H "Authorization: Bearer $SAMO_KEY"
curl "$SAMO_API/companies/clx4a1b2c0001/reviews?page=1&limit=20" -H "Authorization: Bearer $SAMO_KEY"

Only approved reviews are returned, and limit is capped at 50.

Incremental sync

Pull a delta instead of walking the whole exchange:

curl -G "$SAMO_API/proposals/search" \
  -H "Authorization: Bearer $SAMO_KEY" \
  --data-urlencode "updatedSince=2026-08-05T09:00:00.000Z" \
  --data-urlencode "limit=100"

Then page through nextCursor, deduplicating by id. The details and pitfalls are in Pagination and sync.