Boring Order TrackerIntegrations · API documentation

Reference implementation

curl reference client

Download source

These examples require Bash, curl, and jq. Start with the synthetic sandbox. Keep private keys, P12 passwords, claim tokens, link codes and response bodies out of shell history, source control and CI logs. The examples print only data you explicitly request; do not enable curl tracing in production.

Configure mTLS#

For a locally generated key and the PEM certificate-chain download from the portal:

set -euo pipefail
export BOT_API='https://api.integration.boringordertracker.com/v1'
export BOT_CERT='/secure/path/bot-integration-certificate-chain.pem'
export BOT_KEY='/secure/path/bot-integration-client-key.pem'

bot_curl=(
  curl --silent --show-error
  --proto '=https'
  --tlsv1.2
  --cert "$BOT_CERT"
  --key "$BOT_KEY"
  --header 'Accept: application/json'
)

An encrypted PEM key makes curl prompt for its passphrase. For unattended use, provide the passphrase through the service's protected secret/TLS configuration, not as a command-line argument.

For the one-time P12 download, use this array instead. curl prompts for the P12 password because it is deliberately not appended to --cert:

export BOT_API='https://api.integration.boringordertracker.com/v1'
export BOT_P12='/secure/path/bot-integration-certificate.p12'

bot_curl=(
  curl --silent --show-error
  --proto '=https'
  --tlsv1.2
  --cert-type P12
  --cert "$BOT_P12"
  --header 'Accept: application/json'
)

Use only one bot_curl definition. Do not use --insecure.

Capabilities and environment gate#

Always make this the first request for a new or rotated certificate:

"${bot_curl[@]}" --fail-with-body "$BOT_API/capabilities" | jq .

Assert the expected environment, integration status, exact scopes and quotas. For production, stop unless integration_status is active and production_data_enabled is true. A prepared production certificate does not bypass that independent gate.

Keep leading zeroes by treating the code as a string. Reading silently and piping JSON over stdin keeps it out of the command line and process list:

IFS= read -r -s -p 'Ten-digit BOT link code: ' BOT_LINK_CODE
printf '\n'
[[ "$BOT_LINK_CODE" =~ ^[0-9]{10}$ ]] || {
  unset BOT_LINK_CODE
  echo 'The code must contain exactly ten decimal digits.' >&2
  exit 64
}

jq -cn --arg code "$BOT_LINK_CODE" '{code: $code}' \
  | "${bot_curl[@]}" --fail-with-body \
      --header 'Content-Type: application/json' \
      --data-binary @- \
      "$BOT_API/link-codes/redeem" \
  | jq .
unset BOT_LINK_CODE

The code is integration-bound, expires after ten minutes and is consumed once. Do not blindly retry an ambiguous timeout: first reconcile through pull or ask the user to generate a new code. A successful response contains only random, integration-specific connection/resource IDs.

Initial and delta sync with cursor and ETag#

The cursor is opaque. Never decode, modify, compare or manufacture it. Persist one cursor per integration and environment. Persist the ETag alongside the exact cursor/request representation that produced it.

This snippet performs one page request and keeps headers/body in a private temporary directory for inspection by the current process:

umask 077
BOT_TMP_DIR=$(mktemp -d)
trap 'rm -rf "$BOT_TMP_DIR"' EXIT

BOT_SYNC_CURSOR=${BOT_SYNC_CURSOR:-}
BOT_SYNC_ETAG=${BOT_SYNC_ETAG:-}
sync_args=(--get --data-urlencode 'limit=500')

if [[ -n "$BOT_SYNC_CURSOR" ]]; then
  sync_args+=(--data-urlencode "updated_since=$BOT_SYNC_CURSOR")
fi
if [[ -n "$BOT_SYNC_ETAG" ]]; then
  sync_args+=(--header "If-None-Match: $BOT_SYNC_ETAG")
fi

BOT_STATUS=$(
  "${bot_curl[@]}" "${sync_args[@]}" \
    --dump-header "$BOT_TMP_DIR/headers" \
    --output "$BOT_TMP_DIR/body" \
    --write-out '%{http_code}' \
    "$BOT_API/sync"
)

case "$BOT_STATUS" in
  200)
    jq -e '
      (.items | type) == "array" and
      (.next_cursor | type) == "string" and
      (.has_more | type) == "boolean"
    ' "$BOT_TMP_DIR/body" >/dev/null

    BOT_NEXT_CURSOR=$(jq -er '.next_cursor' "$BOT_TMP_DIR/body")
    BOT_HAS_MORE=$(jq -er '.has_more' "$BOT_TMP_DIR/body")
    BOT_NEXT_ETAG=$(
      awk '
        tolower(substr($0, 1, 5)) == "etag:" {
          sub(/\r$/, ""); sub(/^[^:]*:[[:space:]]*/, ""); print
        }
      ' "$BOT_TMP_DIR/headers" | tail -n 1
    )

    # Diagnostic only: these are deletion obligations, not data to retain.
    jq -c '
      .items[]
      | select(.operation == "delete")
      | {connection_id, resource, tombstone}
    ' "$BOT_TMP_DIR/body"

    printf 'Apply %s item(s); has_more=%s\n' \
      "$(jq '.items | length' "$BOT_TMP_DIR/body")" "$BOT_HAS_MORE"
    ;;
  304)
    echo 'No change for this cursor/ETag representation.'
    ;;
  429)
    BOT_RETRY_AFTER=$(
      awk '
        tolower(substr($0, 1, 12)) == "retry-after:" {
          sub(/\r$/, ""); sub(/^[^:]*:[[:space:]]*/, ""); print
        }
      ' "$BOT_TMP_DIR/headers" | tail -n 1
    )
    BOT_RATE_RESET=$(
      awk '
        tolower(substr($0, 1, 16)) == "ratelimit-reset:" {
          sub(/\r$/, ""); sub(/^[^:]*:[[:space:]]*/, ""); print
        }
      ' "$BOT_TMP_DIR/headers" | tail -n 1
    )
    printf 'Rate limited; retry no earlier than %s seconds (reset %s).\n' \
      "$BOT_RETRY_AFTER" "$BOT_RATE_RESET" >&2
    exit 75
    ;;
  *)
    jq . "$BOT_TMP_DIR/body" >&2 || true
    exit 1
    ;;
esac

On 200, apply every item in order and persist BOT_NEXT_CURSOR plus BOT_NEXT_ETAG in the same database transaction. If the transaction fails, keep the previous cursor/ETag and request the page again. When has_more=true, immediately request the next page with the newly committed cursor. When it is false, poll that terminal cursor again after the recommended 15 minutes and send its ETag; 304 has no body and leaves cursor/state unchanged.

An upsert replaces the current allowed representation for its random resource ID. A delete tombstone is an immediate authorization loss: remove the local individual record and all derived copies before acknowledging it. A notification has no individual snapshot. Never infer deletion merely because a resource is absent from one delta page.

If the API returns 410 with code=sync_cursor_gone, the retained delta window no longer contains that cursor. Perform a new no-cursor initial sync into a reconciliation transaction/staging set, follow all initial pages, and remove local resources absent from the new authorized set. Do not continue from a self-created timestamp.

For any 429, parse the integer Retry-After response header and schedule the next attempt no earlier than that many seconds; RateLimit-Reset is a UTC Unix timestamp. Add local jitter when many workers share a certificate. Limits apply across all active certificates for the integration, so switching certificates is not a rate-limit workaround.

Detail and complete history#

Use the random ID from redemption or sync; do not put a VIN, order number or BOT account identifier in the path. A current order snapshot with conditional read:

export BOT_ORDER_ID='ord_random_integration_specific_id'
export BOT_ORDER_ETAG='"etag-from-the-last-response"'

umask 077
BOT_DETAIL_DIR=$(mktemp -d)
trap 'rm -rf "$BOT_DETAIL_DIR"' EXIT
BOT_ORDER_STATUS=$(
  "${bot_curl[@]}" \
    --header "If-None-Match: $BOT_ORDER_ETAG" \
    --dump-header "$BOT_DETAIL_DIR/headers" \
    --output "$BOT_DETAIL_DIR/body" \
    --write-out '%{http_code}' \
    "$BOT_API/orders/$BOT_ORDER_ID"
)

case "$BOT_ORDER_STATUS" in
  200)
    jq -e '
      def config_value($name):
        if ((.configuration? | type) == "object" and (.configuration | has($name)))
        then .configuration[$name]
        else .[$name]
        end;
      .resource.type as $type
      | .data
      | if $type == "order" and (has("delivery_center") | not)
        then error("order is missing nullable delivery_center")
        else .
        end
      | if ((.option_codes // []) | all(type == "string"))
        then {
          model,
          variant,
          delivery_center: (if $type == "order" then .delivery_center else null end),
          option_codes: (.option_codes // []),
          configuration: {
            color: config_value("color"),
            wheels: config_value("wheels"),
            interior: config_value("interior"),
            autopilot: config_value("autopilot"),
            tow_hitch: config_value("tow_hitch")
          }
        }
        else error("option_codes must be an array of strings")
        end
    ' "$BOT_DETAIL_DIR/body"
    ;;
  304) echo 'Order snapshot is unchanged.' ;;
  410)
    echo 'Authorization was revoked; delete locally and obtain the ACK ID from sync.' >&2
    exit 1
    ;;
  *) jq . "$BOT_DETAIL_DIR/body" >&2 || true; exit 1 ;;
esac

The projection deliberately consumes BOT's returned variant and readable configuration values. It preserves null and never guesses a trim, colour, wheel or interior from a raw option code. delivery_center is mandatory as a key for orders, although its documented value may be null.

History has its own resource-bound opaque cursor and ETag. Request the first page without cursor, then repeat with each returned next_cursor until has_more=false:

BOT_HISTORY_CURSOR=${BOT_HISTORY_CURSOR:-}
BOT_HISTORY_ETAG=${BOT_HISTORY_ETAG:-}
history_args=(--get --data-urlencode 'limit=500')

if [[ -n "$BOT_HISTORY_CURSOR" ]]; then
  history_args+=(--data-urlencode "cursor=$BOT_HISTORY_CURSOR")
fi
if [[ -n "$BOT_HISTORY_ETAG" ]]; then
  history_args+=(--header "If-None-Match: $BOT_HISTORY_ETAG")
fi

"${bot_curl[@]}" "${history_args[@]}" --fail-with-body \
  "$BOT_API/orders/$BOT_ORDER_ID/history" \
  | jq .

Commit the ordered history items and the returned cursor/ETag atomically just like sync. history_complete=false is a real incomplete state: do not present the downloaded prefix as the complete change history. Vehicle endpoints use the same contract at /vehicles/{id} and /vehicles/{id}/history. A detail/history 410 means authorization was revoked; delete the local resource and use sync to obtain the revocation ID needed for acknowledgement.

Deletion acknowledgement#

Only acknowledge after the deletion workflow has removed every retained copy covered by the revocation, including caches, search indexes, exports, queued payloads and derived individual data. Use the revocation_id from the sync tombstone and reuse one stable idempotency key for every retry:

export BOT_REVOCATION_ID='rev_id_from_sync_tombstone'
export BOT_DELETION_COMPLETED='1'
[[ "$BOT_REVOCATION_ID" =~ ^[A-Za-z0-9_-]{8,80}$ ]]
[[ "$BOT_DELETION_COMPLETED" == '1' ]]

BOT_ACK_KEY="ack-$BOT_REVOCATION_ID"
"${bot_curl[@]}" --fail-with-body \
  --header 'Content-Type: application/json' \
  --header "Idempotency-Key: $BOT_ACK_KEY" \
  --data-binary '{"deletion_completed":true}' \
  "$BOT_API/revocations/$BOT_REVOCATION_ID/ack" \
  | jq .

The same revocation/key pair is safely retryable. Reusing that key for a different revocation returns a conflict. A successful acknowledgement is an operator assertion that deletion is complete; it is not a command asking BOT to delete the operator's data.

Static sandbox fixture

Expected result

This production-shaped response is embedded directly in the documentation. Opening this page does not call the API, redeem a code, use a certificate, or access live data. Your integration should accept the same documented shape while treating IDs, dates, revisions, and values as variable.

{
  "resource": {
    "type": "order",
    "id": "ord_sb_A7mQ2pL9xK4v"
  },
  "revision": 42,
  "history_complete": true,
  "data": {
    "model": "Model Y",
    "variant": "Long Range AWD",
    "factory": "Gigafactory Berlin-Brandenburg",
    "factory_code": "GF4",
    "option_codes": [
      "APBS",
      "IPB0",
      "MDLY",
      "MTY03",
      "PN00",
      "TW01",
      "WY20P"
    ],
    "configuration": {
      "color": "Quicksilver",
      "wheels": "Induction 20-inch",
      "interior": "All Black",
      "autopilot": "Autopilot",
      "tow_hitch": true
    },
    "delivery_center": "Düsseldorf-Holthausen - SANDBOX",
    "country": "DE",
    "country_code": "DE",
    "order_date": "2026-04-14",
    "reservation_date": "2026-04-14",
    "order_booked_date": "2026-04-16",
    "expected_start_date": "2026-09-18",
    "expected_end_date": "2026-09-29",
    "expected_delivery": "18 September - 29 September 2026",
    "vin_assigned": true,
    "vin_assigned_on": "2026-07-19T10:12:00Z",
    "vehicle_built": true,
    "vehicle_built_on": "2026-07-22T08:45:00Z",
    "in_production_at": "2026-07-15T07:30:00Z",
    "in_transit_at": "2026-07-24T12:05:00Z",
    "status": "in_transit",
    "last_delivery_window_change_at": "2026-08-11T09:16:00Z"
  }
}

Expected parsed values: Model Y · Long Range AWD · Quicksilver · Induction 20-inch · Düsseldorf-Holthausen - SANDBOX · 7 option codes. Missing allowed values may still be null; never infer them.