# curl

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:

```bash
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`:

```bash
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:

```bash
"${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.

## Redeem a ten-digit link code

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:

```bash
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:

```bash
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:

```bash
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 . "$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
```

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`:

```bash
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:

```bash
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.
