Custody Permits
Create and manage custody permits (authorizations) for vehicle custody transfers.
SDK-Only Access
To ensure your integration remains stable and supported, always use the official SDKs. The underlying API endpoints are internal implementation details that may change in purpose, functionality, or be removed at any time without notice. Direct API access is not supported. Learn more →
The SDK methods documented here are your supported interface. Versioning is managed through SDK upgrades.
Overview
Custody permits authorize specific parties to take custody of vehicles. Each authorization specifies:
- VIN: The vehicle being authorized
- Route: Origin and destination locations (OD pair)
- Person Identity: Who can take custody (from VECU IDV SDK)
- Transport Order: Optional external shipment/order reference
- Validity Period: When the authorization is valid
Multi-driver authorization (ADR-042)
An authorization can carry one driver (the default single_driver mode)
or a list of authorized drivers that resolves to one assigned driver later
(multi_driver mode). Multi-driver mode is opt-in per client and introduces
three operations beyond the single-driver shape: an extended
CreateAuthorizationAsync() accepting an AuthorizedDrivers list,
UpdateAuthorizationDriversAsync() to add or remove drivers, and
AssignAuthorizationToDriverAsync() to lock the authorization to one driver.
See the Multi-Driver Integration Guide for
end-to-end flows.
Methods
CreateAuthorizationAsync()
Create a new custody authorization for a vehicle.
requestCreateAuthorizationRequestrequiredAuthorization creation request containing VIN, origin/destination, and identity information
cancellationTokenCancellationTokenOptional cancellation token for the async operation
CreateAuthorizationRequest Properties
Vinstringrequired17-character Vehicle Identification Number (excludes I, O, Q)
OriginstringrequiredOrigin facility ID
DestinationstringrequiredDestination facility ID
PersonIdentityKeystringrequiredPerson identity credential from VECU IDV SDK
MakeModelstringrequiredVehicle make and model (e.g., "Honda Accord", "Ford F-150")
AuthorizedBystringrequiredSystem or user ID initiating the request
TransportOrderIdstring?External transport order or shipment ID (optional)
ValidUntilDateTimeOffset?Authorization expiration timestamp in ISO 8601 format (default: 24 hours from creation)
RoleAuthorizationRole?Role of the authorized party: OWNER, ADMIN, or DRIVER
PermittedActionsList<string>?Optional list of permitted actions for this authorization
BuyerIdentityKeystring?Optional buyer identity key
OrganizationIdstring?Optional organization identifier (2–128 chars, pattern
^[A-Za-z0-9][A-Za-z0-9_\-.]*[A-Za-z0-9]$). Per
ADR-042,
OrganizationId is caller-asserted forensic metadata only — accepted,
stored verbatim on the authorization record, and echoed on events and 409
envelopes for downstream forensic correlation. It does NOT participate in
marker uniqueness or the 409 disclosure guard. Strict (VIN, geohash)
uniqueness is enforced unconditionally; ADR-040's
VinOriginSameCompanyAllowed scope was superseded by ADR-042 and removed.
AuthorizedDriversList<AuthorizedDriver>?Multi-driver mode (ADR-042) — list of drivers eligible to take custody; one
will be locked in via AssignAuthorizationToDriverAsync() later. Mutually
exclusive with PersonIdentityKey — supplying both returns 400
CONFLICTING_DRIVER_FIELDS; supplying neither returns 400 MISSING_DRIVER.
Requires the calling client to be configured for multi_driver mode
(otherwise 400 MULTI_DRIVER_MODE_REQUIRED). Bounded by per-client cap
(default 25, configurable down per-client but not up — exceeding returns 429
DRIVER_LIMIT_EXCEEDED).
ActorIdentifierstring?Caller-asserted forensic identifier for the internal user inside your system
that initiated the request (ADR-042 §Decision §9). Stored verbatim and
emitted on custody.authorization.created, .modified, and .assigned
events; NOT verified or validated by VECU. Useful when your client
serves multiple internal users and you want a correlation hook into your own
audit logs.
Returns: AuthorizationResponse with authorization details and status
Exceptions:
ValidationException- Invalid request parameters (400, 422)InvalidOriginAddressException- Origin address rejected by the geocoder (400, ErrorCode=INVALID_ORIGIN_ADDRESS)AuthException- Authentication failure (401)DuplicateAuthorizationException- Authorization already exists for(VIN, canonicalized origin)(409). Per ADR-042 the disclosure guard issame_clientonly — cross-tenant collisions return the conflict but omitExistingAuthorizationand theLocationheader.ExistingAuthorizationis nullable — see Error Handling.VehicleNotReleasableException- Vehicle cannot be released due to business rules (409)RateLimitException- Rate limit exceeded (429)PoolCapacityExceededException- Authorization pool at capacity for this VIN under the effective uniqueness scope (429, ErrorCode=POOL_CAPACITY_EXCEEDED)DriverLimitExceededException- Multi-driver POST exceeded per-client cap (429, ErrorCode=DRIVER_LIMIT_EXCEEDED). Body carriesAttemptedCount+MaxAllowed. NO state changes applied (atomic pre-write rejection).MultiDriverModeRequiredException- Caller's client is insingle_drivermode butAuthorizedDriverswas supplied (400, ErrorCode=MULTI_DRIVER_MODE_REQUIRED). Subclass ofInvalidConfigurationException— catch the parent class to handle the whole config-error family.PoolFanOutException- Multi-driver POST pool fan-out failed and was rolled back (503, ErrorCode=POOL_FANOUT_FAILURE). Caller can retry the entire POST. Body carriesSucceededDriverCount,RolledBackEntries, andRollbackFailures(composite IDs left orphaned at auth-pool — ops attention required).GeocodingServiceUnavailableException- Upstream geocoder unavailable; SDK has exhausted its retry budget (503, ErrorCode=GEOCODING_SERVICE_UNAVAILABLE)NetworkException- Network or server errors (5xx)
Example:
using CoxAuto.Vecu.CustodySdk.Client;
using CoxAuto.Vecu.CustodySdk.Models.Requests;
using CoxAuto.Vecu.CustodySdk.Exceptions;
public class AuthorizationService
{
private readonly ICustodyServiceClient _client;
public AuthorizationService(ICustodyServiceClient client)
{
_client = client;
}
public async Task<string> CreateAuthorizationAsync()
{
var request = new CreateAuthorizationRequest
{
Vin = "1HGBH41JXMN109186",
Origin = "1 Auction Blvd, Bordentown, NJ 08505",
Destination = "200 Motor Ave, Columbus, OH 43230",
PersonIdentityKey = "DL-293-847-561",
MakeModel = "Honda Accord",
AuthorizedBy = "CENTRAL_DISPATCH",
TransportOrderId = "TRO-2024-00472",
ValidUntil = DateTimeOffset.UtcNow.AddHours(48),
Role = AuthorizationRole.DRIVER
};
try
{
var response = await _client.CreateAuthorizationAsync(request);
Console.WriteLine($"Authorization created: {response.AuthorizationId}");
Console.WriteLine($"Status: {response.Status}");
Console.WriteLine($"Created at: {response.CreatedAt}");
Console.WriteLine($"Expires at: {response.ExpiresAt}");
Console.WriteLine($"Effective scope: {response.UniquenessScope}");
Console.WriteLine($"Origin (formatted): {response.OriginFormattedAddress}");
Console.WriteLine($"Origin geohash: {response.OriginGeohash}");
Console.WriteLine($"Location header: {response.LocationUri}");
return response.AuthorizationId;
}
catch (DuplicateAuthorizationException ex)
{
// ExistingAuthorization is nullable as of v3.2.0 — gate on the
// null check ALONE. Per ADR-040 §7.1, do NOT infer
// disclosure-guard denial from a null value (it can also be a
// transient enrichment failure).
if (ex.ExistingAuthorization is not null)
{
Console.WriteLine($"Existing authorization: {ex.ExistingAuthorization.AuthorizationId}");
}
else
{
Console.WriteLine($"Conflict on VIN/origin; existing record not surfaced. EffectiveScope={ex.EffectiveScope}");
}
throw;
}
catch (VehicleNotReleasableException ex)
{
Console.WriteLine($"Vehicle not releasable: {ex.Message}");
throw;
}
}
}
GetAuthorizationAsync()
Retrieve detailed information about an authorization by ID.
authorizationIdstringrequiredUnique authorization identifier (e.g., "AUTH-xyz789")
cancellationTokenCancellationTokenOptional cancellation token for the async operation
Returns: AuthorizationDetails with full authorization information
Exceptions:
ArgumentException- Authorization ID is null or whitespaceAuthException- Authentication failure (401)AuthorizationNotFoundException- Authorization not found (404)NetworkException- Network or server errors (5xx)
Example:
using CoxAuto.Vecu.CustodySdk.Exceptions;
public async Task CheckAuthorizationStatus(string authorizationId)
{
try
{
var details = await _client.GetAuthorizationAsync(authorizationId);
Console.WriteLine($"Authorization: {details.AuthorizationId}");
Console.WriteLine($"VIN: {details.Vin}");
Console.WriteLine($"Status: {details.Status}");
Console.WriteLine($"Origin: {details.Origin}");
Console.WriteLine($"Destination: {details.Destination}");
Console.WriteLine($"Role: {details.Role}");
Console.WriteLine($"Expires at: {details.ExpiresAt}");
if (details.CancelledAt != null)
{
Console.WriteLine($"Cancelled at: {details.CancelledAt}");
Console.WriteLine($"Cancelled by: {details.CancelledBy}");
Console.WriteLine($"Reason: {details.CancellationReason}");
}
}
catch (AuthorizationNotFoundException ex)
{
Console.WriteLine($"Authorization not found: {ex.Message}");
}
}
CancelAuthorizationAsync()
Cancel an existing authorization.
authorizationIdstringrequiredUnique authorization identifier to cancel
requestCancelAuthorizationRequestrequiredCancellation request with reason and optional notes
cancellationTokenCancellationTokenOptional cancellation token for the async operation
CancelAuthorizationRequest Properties
CancellationReasonstringrequiredFree-form reason for cancellation (minimum 10 characters)
Returns: CancelAuthorizationResponse with cancellation details
Exceptions:
ArgumentException- Authorization ID is null or whitespaceArgumentNullException- Request is nullAuthException- Authentication failure (401)AuthorizationNotFoundException- Authorization not found (404)ValidationException- Authorization cannot be cancelled (422)NetworkException- Network or server errors (5xx)
Example:
using CoxAuto.Vecu.CustodySdk.Models.Requests;
public async Task CancelAuthorization(string authorizationId)
{
var request = new CancelAuthorizationRequest
{
CancellationReason = "Driver reassigned to different route due to scheduling conflict"
};
try
{
var response = await _client.CancelAuthorizationAsync(
authorizationId,
request);
Console.WriteLine($"Authorization cancelled: {response.AuthorizationId}");
Console.WriteLine($"VIN: {response.Vin}");
Console.WriteLine($"Cancelled at: {response.CancelledAt}");
Console.WriteLine($"Cancelled by: {response.CancelledBy}");
Console.WriteLine($"Status: {response.Status}");
Console.WriteLine($"Reason: {response.CancellationReason}");
}
catch (ValidationException ex)
{
Console.WriteLine($"Cannot cancel: {ex.Message}");
// Authorization might already be cancelled, expired, or in use
}
}
WaitForAuthorizationAsync()
Wait for an authorization to reach a terminal status by polling.
authorizationIdstringrequiredUnique authorization identifier to wait for
optionsWaitOptions?Optional wait configuration (polling interval, timeout, custom predicate)
cancellationTokenCancellationTokenOptional cancellation token for the async operation
WaitOptions Properties
PollingIntervalSecondsintPolling interval in seconds (1-60, default: 5)
TimeoutSecondsintMaximum wait timeout in seconds (1-3600, default: 300)
CustomPredicateFunc<AuthorizationDetails, bool>?Optional custom completion condition
Returns: AuthorizationDetails when terminal status or custom condition is reached
Terminal Statuses: RELEASED, CANCELLED, EXPIRED, REVOKED
Exceptions:
ArgumentException- Authorization ID is null or whitespaceAuthException- Authentication failure (401)AuthorizationNotFoundException- Authorization not found (404)TimeoutException- Wait timeout exceeded before reaching terminal statusOperationCanceledException- Operation cancelled via cancellation tokenNetworkException- Network or server errors (5xx)
Example:
using CoxAuto.Vecu.CustodySdk.Client;
public async Task WaitForAuthorization(string authorizationId)
{
try
{
// Wait with default options (5s interval, 5min timeout)
var details = await _client.WaitForAuthorizationAsync(authorizationId);
Console.WriteLine($"Authorization reached terminal status: {details.Status}");
Console.WriteLine($"Expires at: {details.ExpiresAt}");
}
catch (TimeoutException)
{
Console.WriteLine("Authorization did not reach terminal status within timeout");
}
}
public async Task WaitWithCustomOptions(string authorizationId)
{
// Custom wait options
var options = new WaitOptions
{
PollingIntervalSeconds = 10, // Check every 10 seconds
TimeoutSeconds = 600, // Wait up to 10 minutes
CustomPredicate = auth => auth.Status == AuthorizationStatus.RELEASED
};
var cts = new CancellationTokenSource(TimeSpan.FromMinutes(10));
try
{
var details = await _client.WaitForAuthorizationAsync(
authorizationId,
options,
cts.Token);
Console.WriteLine($"Authorization released: {details.AuthorizationId}");
}
catch (TimeoutException)
{
Console.WriteLine("Authorization was not released within timeout");
}
catch (OperationCanceledException)
{
Console.WriteLine("Wait operation was cancelled");
}
}
UpdateAuthorizationDriversAsync()
Add or remove drivers on a multi-driver authorization
(ADR-042
§Decision §6). Available only when the authorization was created
under multi_driver mode and the calling client owns the authorization.
ETag-guarded for optimistic concurrency, idempotent on add/remove
operations.
authorizationIdstringrequiredAuthorization ID (canonical UUID).
requestUpdateDriversRequestrequiredAdd list, remove list, current Version (sent as If-Match), and optional
ActorIdentifier.
cancellationTokenCancellationTokenOptional cancellation token for the async operation.
UpdateDriversRequest Properties
VersionintrequiredCurrent monotonic version of the authorization (the value last returned by
CreateAuthorizationAsync, GetAuthorizationAsync, or a prior
UpdateAuthorizationDriversAsync / AssignAuthorizationToDriverAsync
call). The SDK serializes this int as the quoted ETag form on the wire
(e.g., If-Match: "3") per RFC 7232 — callers bypassing the SDK should
match this convention. A stale version returns 412 Precondition Failed;
the SDK surfaces this as StaleETagException with CurrentVersion.
AddList<AuthorizedDriver>?Drivers to add to the list. Idempotent — adding a driver already on the list
is a no-op. Per-driver Name is display-only and does NOT appear in event
payloads (per ADR-042 AC-16).
RemoveList<string>?Person identity keys to remove. Idempotent — removing a driver not on the list is a no-op. Removed drivers' pool entries are cancelled best-effort.
ActorIdentifierstring?Caller-asserted forensic identifier; emitted on
custody.authorization.modified. Not verified.
Returns: UpdateDriversResponse with the new Version and the
post-PATCH AuthorizedDrivers list.
Exceptions:
MultiDriverModeRequiredException- 400, the authorization was created undersingle_drivermode (subclass ofInvalidConfigurationException).NotAuthorizationOwnerException- 403, caller'sclient_iddoes not match the authorization'sclient_id.EmptyDriverListException- 400, net effect of the PATCH would leave zero drivers. NO state changes.AuthorizationAlreadyAssignedException- 409, authorization is already ASSIGNED — driver-list modifications no longer permitted.StaleETagException- 412, version mismatch. Refetch viaGetAuthorizationAsyncand retry.DriverLimitExceededException- 429, proposed list (after applying add/remove) exceeds per-client cap. NO state changes.AuthException- 401, invalid or missing JWT.NetworkException- 5xx network or server errors.
Example:
using CoxAuto.Vecu.CustodySdk.Models.Requests;
using CoxAuto.Vecu.CustodySdk.Models;
using CoxAuto.Vecu.CustodySdk.Exceptions;
public async Task UpdateDriversExample(string authorizationId, int currentVersion)
{
var request = new UpdateDriversRequest
{
Version = currentVersion, // ETag from prior fetch
Add = new List<AuthorizedDriver>
{
new()
{
PersonIdentityKey = "vecu_dAvEpKxYz1234567",
Name = "Dave Driver",
},
},
Remove = new List<string> { "vecu_gfTRAjYnn_y-8zj-aBc4dEf5" }, // Alice's personIdentityKey
ActorIdentifier = "ops-user-42@client.example.com",
};
try
{
var response = await _client.UpdateAuthorizationDriversAsync(
authorizationId, request);
Console.WriteLine($"New version: {response.Version}");
Console.WriteLine($"Drivers now: {response.AuthorizedDrivers.Count}");
}
catch (StaleETagException ex)
{
// Concurrent PATCH bumped the version — refetch and retry
var fresh = await _client.GetAuthorizationAsync(authorizationId);
Console.WriteLine($"Stale version; current is {fresh.Version}");
}
catch (MultiDriverModeRequiredException)
{
Console.WriteLine("Authorization was not created under multi_driver mode");
}
catch (EmptyDriverListException)
{
Console.WriteLine("PATCH would leave no drivers");
}
catch (AuthorizationAlreadyAssignedException)
{
Console.WriteLine("Cannot modify drivers after AssignAuthorizationToDriverAsync");
}
}
AssignAuthorizationToDriverAsync()
Lock the authorization to a single chosen driver
(ADR-042
§Decision §7). Transitions the authorization to ASSIGNED, cancels
non-assigned pool entries, and revokes credentials issued for
non-assigned drivers. Sync-blocking: by the time the call returns,
all non-assigned credentials are revoked at the credential service.
Available in both modes
AssignAuthorizationToDriverAsync works on both single_driver and
multi_driver authorizations. On a single-driver authorization the call is a
no-op-shaped confirmation of the existing 1-driver list.
authorizationIdstringrequiredAuthorization ID (canonical UUID).
requestAssignAuthorizationRequestrequiredChosen PersonIdentityKey, current Version, optional ActorIdentifier.
cancellationTokenCancellationTokenOptional cancellation token.
AssignAuthorizationRequest Properties
VersionintrequiredCurrent monotonic version. Same ETag semantics as
UpdateAuthorizationDriversAsync.
PersonIdentityKeystringrequiredMapped person identity key of the chosen driver. MUST be on the
authorization's AuthorizedDrivers list — otherwise 400
DRIVER_NOT_ON_LIST.
ActorIdentifierstring?Caller-asserted forensic identifier; emitted on
custody.authorization.assigned. Not verified.
Returns: AssignAuthorizationResponse with AssignedDriverPik,
new Version, RevokedCredentialIds (credentials revoked as part of
the assign — empty pre-acceptance), and DriverAuthorizationMode.
Exceptions:
DriverNotOnListException- 400, requestedPersonIdentityKeyis not on the authorization'sAuthorizedDriverslist. UseUpdateAuthorizationDriversAsyncfirst to add.NotAuthorizationOwnerException- 403, caller does not own the authorization.AuthorizationAlreadyAssignedToDifferentDriverException- 409, the authorization is already ASSIGNED to a different driver. Cancel-and-recreate is the only path to change the assigned driver.AuthorizationNotAssignableException- 409, the authorization is in a terminal status (RELEASED,EXPIRED,REVOKED,CANCELLED).StaleETagException- 412, version mismatch.AssignPartialFailureException- 503ASSIGN_PARTIAL_FAILURE. The authorization IS assigned; credential revocation completed only partially. Safe to retry with the bumped version (revocation is idempotent on the credential-service side). Body carriesRevokedCredentialIds(succeeded) andFailedRevocations(each withCredentialId+Reason).
Idempotency: assigning the already-assigned driver to an already-ASSIGNED authorization returns 200 with the existing state and a fresh ETag.
Example:
using CoxAuto.Vecu.CustodySdk.Models.Requests;
using CoxAuto.Vecu.CustodySdk.Exceptions;
public async Task AssignDriver(string authorizationId, int currentVersion)
{
var request = new AssignAuthorizationRequest
{
Version = currentVersion,
PersonIdentityKey = "vecu_gfTRAjYnn_y-8zj-aBc4dEf5", // Alice
ActorIdentifier = "ops-user-42@client.example.com",
};
try
{
var response = await _client.AssignAuthorizationToDriverAsync(
authorizationId, request);
Console.WriteLine($"Status: {response.Status}"); // ASSIGNED
Console.WriteLine($"Assigned driver: {response.AssignedDriverPik}");
Console.WriteLine(
$"Revoked credentials: {string.Join(", ", response.RevokedCredentialIds)}");
}
catch (DriverNotOnListException ex)
{
Console.WriteLine(
$"Driver {ex.RequestedPersonIdentityKey} not on list; " +
"call UpdateAuthorizationDriversAsync first.");
}
catch (AuthorizationAlreadyAssignedToDifferentDriverException)
{
Console.WriteLine("Already ASSIGNED to a different driver.");
}
catch (AssignPartialFailureException ex)
{
// Authorization IS assigned; credential-revoke partially failed.
// Safe to retry with the bumped version.
Console.WriteLine(
$"Partial revocation: {ex.RevokedCredentialIds.Count} ok, " +
$"{ex.FailedRevocations.Count} failed");
var retryRequest = request with { Version = ex.CurrentVersion };
await _client.AssignAuthorizationToDriverAsync(authorizationId, retryRequest);
}
}
AddOrCreateDriverAsync()
SDK-side convenience wrapper
(ADR-042
§Decision §10). Tries CreateAuthorizationAsync first; on 409
DUPLICATE_AUTHORIZATION for the same (VIN, origin), falls back to
listing existing authorizations (auto-scoped to the caller's
client_id) and UpdateAuthorizationDriversAsync to add the driver.
Returns a unified result indicating which path was taken.
requestAddOrCreateDriverRequestrequiredVIN, origin, destination, the single driver to add or create with,
AuthorizedBy, MakeModel, optional ActorIdentifier, optional
MaxRetries (default 3 for ETag-mismatch retries on the PATCH fallback
path).
cancellationTokenCancellationTokenOptional cancellation token.
Returns: AddOrCreateDriverResult with:
Authorization— the resulting authorization.Created—trueif the POST path was taken;falseif the PATCH fallback was taken.Added—trueifUpdateAuthorizationDriversAsyncactually added the driver;falseif the driver was already on the existing list (idempotent no-op).
Exceptions: any exception raised by CreateAuthorizationAsync or
UpdateAuthorizationDriversAsync. Only the specific 409
DUPLICATE_AUTHORIZATION case triggers the PATCH fallback — other 409
reasons (e.g., AUTHORIZATION_ALREADY_ASSIGNED) bubble up unchanged.
ETag-mismatch retries are bounded; exhaustion raises
RetryExhaustedException.
Example:
using CoxAuto.Vecu.CustodySdk.Models.Requests;
using CoxAuto.Vecu.CustodySdk.Models;
using CoxAuto.Vecu.CustodySdk.Exceptions;
public async Task AddOrCreate(string vin, string driverPik)
{
var request = new AddOrCreateDriverRequest
{
Vin = vin,
Origin = "1 Auction Blvd, Bordentown, NJ 08505",
Destination = "200 Motor Ave, Columbus, OH 43230",
Driver = new AuthorizedDriver
{
PersonIdentityKey = driverPik,
Name = "Alice Driver",
},
AuthorizedBy = "dispatch-system",
MakeModel = "Honda Accord 2023",
ActorIdentifier = "ops-user-42@client.example.com",
};
try
{
var result = await _client.AddOrCreateDriverAsync(request);
if (result.Created)
{
Console.WriteLine(
$"Created new authorization {result.Authorization.AuthorizationId}");
}
else if (result.Added)
{
Console.WriteLine(
$"Added driver to existing authorization {result.Authorization.AuthorizationId}");
}
else
{
Console.WriteLine("Driver was already on the list (no-op)");
}
}
catch (RetryExhaustedException)
{
// ETag conflicts persisted past the retry budget — concurrent
// modification by another caller
Console.WriteLine("Concurrent modifications exhausted retry budget");
}
}
Response Objects
AuthorizationResponse
Response after creating an authorization. Returned on 201 Created. On
409 Conflict, embedded as
DuplicateAuthorizationException.ExistingAuthorization only when the
disclosure guard passes (same client) AND service-side enrichment
succeeded. Per
ADR-042,
the disclosure guard simplified to same_client only — cross-tenant
collisions return the conflict but omit ExistingAuthorization and
the Location header. The service still falls back to the minimal
envelope on enrichment failure even when the guard would otherwise
pass, so a null ExistingAuthorization does not by itself imply a
cross-tenant conflict.
AuthorizationIdstringUnique authorization identifier
VinstringVehicle Identification Number
StatusAuthorizationStatusCurrent status (PENDING, ASSIGNED, RELEASED, REVOKED, EXPIRED,
CANCELLED). ASSIGNED (ADR-042) is a refinement of PENDING set by
AssignAuthorizationToDriverAsync() — it is NOT a terminal status; the
existing transitions to RELEASED|EXPIRED|REVOKED|CANCELLED still apply
from ASSIGNED.
OriginstringOrigin facility ID as submitted on the request (raw, pre-canonicalization)
DestinationstringDestination facility ID
CreatedAtDateTimeOffsetAuthorization creation timestamp
ExpiresAtDateTimeOffsetAuthorization expiration timestamp
UniquenessScopeUniquenessScopeDeprecated under
ADR-042.
Always returns VinOriginStrict for new authorizations — strict (VIN, geohash) uniqueness is the unconditional invariant. Pre-ADR-042 records may
carry historical values (VinOriginSameCompanyAllowed) for forensic
reasons. Will be removed from the response in a future SDK major version; do
not branch on this value going forward.
OrganizationIdstring?Echoed back if supplied on the request — caller-asserted forensic metadata only per ADR-042. Does NOT participate in marker uniqueness or 409 disclosure-guard semantics.
OriginGeohashstring?Precision-7 geohash of the canonicalized origin address (≈152 m × 152 m
cell) per ADR-041. The marker key the service uses for uniqueness is derived
from this geohash, not from the raw Origin string. Nullable for
migration-window and pre-ADR-041 terminal records that were backfilled
without geohash data; populated on all new responses.
OriginFormattedAddressstring?Amazon Location Service canonical address string for the geocoded origin.
Surface this back to end users when echoing the authorization so they see
the address as the system stored it. Nullable for the same
migration-window reasons as OriginGeohash.
LocationUriUri?Parsed value of the Location response header. Present on 201 Created. On
409 Conflict, present only when the disclosure guard passes (same
client) AND service-side enrichment succeeded (per ADR-042 — the prior
same-company disclosure clause was removed); null on cross-tenant
conflicts and on enrichment fail-closed cases. Tracks the same nullability
semantics as DuplicateAuthorizationException.ExistingAuthorization. When
populated, points at GET /v1/authorizations/{id} for the authoritative
authorization (newly created or pre-existing duplicate).
AuthorizedDriversList<AuthorizedDriver>?Multi-driver authorizations (ADR-042) — the current list of drivers eligible
to take custody. Empty / absent on single-driver authorizations. Each entry
exposes PersonIdentityKey (mapped HMAC form per ADR-043) and Name
(display-only — does NOT appear in event payloads per AC-16).
Versionint?Multi-driver authorizations (ADR-042) — monotonic version number incremented
on every UpdateAuthorizationDriversAsync and
AssignAuthorizationToDriverAsync call. Use as the Version argument on
subsequent modification calls (sent as If-Match). Single-driver
authorizations return version 1 and never increment.
DriverAuthorizationModestring?Multi-driver authorizations (ADR-042) — single_driver or multi_driver.
Stamped at authorization-creation time and frozen on the record; the calling
client's live config does not retroactively change the mode of existing
authorizations.
ActorIdentifierstring?Caller-asserted forensic identifier (ADR-042 §Decision §9). Reflects the most recent value the caller supplied — preserved across omitted-actor calls. Not verified.
AssignedDriverPikstring?Multi-driver authorizations only — the chosen driver's raw
PersonIdentityKey once AssignAuthorizationToDriverAsync has been called.
Absent when the authorization is still PENDING (no assign yet). Server- side
identity-mapping per ADR-043 happens in the event pipeline, not the response
— the response surfaces the raw value the caller supplied.
AuthorizationDetails
Detailed authorization information.
AuthorizationIdstringUnique authorization identifier
StatusAuthorizationStatusCurrent status (PENDING, ASSIGNED, RELEASED, REVOKED, EXPIRED,
CANCELLED). ASSIGNED is a refinement of PENDING introduced by ADR-042
— not a terminal status.
VinstringVehicle Identification Number
OriginstringOrigin facility ID
DestinationstringDestination facility ID
RoleAuthorizationRoleRole of the authorized party: OWNER, ADMIN, or DRIVER
CreatedAtDateTimeOffsetCreation timestamp
UpdatedAtDateTimeOffsetLast update timestamp
ExpiresAtDateTimeOffsetExpiration timestamp
PoolIdstring?Authorization pool ID (if applicable)
AuthPoolAuthorizationIdstring?Pool authorization ID (if applicable)
CancelledAtDateTimeOffset?Cancellation timestamp (null if not cancelled)
CancelledBystring?User or system that cancelled the authorization (null if not cancelled)
CancellationReasonstring?Reason for cancellation (null if not cancelled)
CancelAuthorizationResponse
Response after cancelling an authorization.
AuthorizationIdstringAuthorization ID that was cancelled
VinstringVehicle Identification Number
StatusAuthorizationStatusStatus after cancellation (CANCELLED)
CancelledAtDateTimeOffsetCancellation timestamp
CancelledBystringUser or system that cancelled the authorization
CancellationReasonstringReason for cancellation
AuthorizedDriver (ADR-042)
Per-driver entry on a multi-driver authorization.
PersonIdentityKeystringrequiredMapped person identity key (HMAC form per ADR-043). Used as the key for add / remove / assign operations.
Namestring?Display-only label (e.g., "Alice Driver"). Stored on the authorization
record but does NOT appear in event payloads per ADR-042 AC-16
(custody.authorization.modified and custody.authorization.assigned carry
mapped personIdentityKey values only). Useful for populating UI without
re-querying upstream identity systems.
UpdateDriversResponse (ADR-042)
Returned by UpdateAuthorizationDriversAsync().
AuthorizationIdstringAuthorization ID that was modified.
VinstringVehicle Identification Number.
StatusAuthorizationStatusAlways PENDING post-PATCH (PATCH is rejected with 409
AUTHORIZATION_ALREADY_ASSIGNED if the authorization is in any other
state).
AuthorizedDriversList<AuthorizedDriver>Post-PATCH driver list (state after add/remove operations applied).
VersionintNew monotonic version. Use as the next Version argument.
DriverAuthorizationModestringStamped mode (multi_driver for any authorization that supports PATCH).
ActorIdentifierstring?Caller-asserted forensic identifier (preserved across omitted-actor calls). Not verified.
AssignAuthorizationResponse (ADR-042)
Returned by AssignAuthorizationToDriverAsync(). Extends the standard
authorization shape with revocation telemetry from the sync-blocking
AC-12 contract.
AuthorizationIdstringAuthorization ID that was assigned.
VinstringVehicle Identification Number.
StatusAuthorizationStatusASSIGNED on success.
AssignedDriverPikstringRaw PersonIdentityKey of the chosen driver (server-side identity-mapping
per ADR-043 happens in the event pipeline, not the response).
VersionintNew monotonic version after the assign committed.
DriverAuthorizationModestringStamped mode (single_driver or multi_driver).
ActorIdentifierstring?Caller-asserted forensic identifier (preserved if not supplied on this call).
RevokedCredentialIdsList<string>Credential IDs revoked at the credential service as part of the assign.
Empty list when no non-assigned drivers had issued credentials
(pre-acceptance assigns). On 503 ASSIGN_PARTIAL_FAILURE, this list
contains only the credentials that succeeded — the failures live on
AssignPartialFailureException.FailedRevocations.
Common Use Cases
Auction to Dealership Transfer
public async Task<string> CreateAuctionTransfer(
string vin,
string driverIdentity)
{
var request = new CreateAuthorizationRequest
{
Vin = vin,
Origin = "1180 Lake Hearn Dr NE, Atlanta, GA 30342",
Destination = "4101 Millenia Blvd, Orlando, FL 32839",
PersonIdentityKey = driverIdentity,
MakeModel = "Toyota Camry",
TransportOrderId = $"TO-{DateTime.UtcNow:yyyyMMdd}-001",
AuthorizedBy = "auction-integration-service",
ValidUntil = DateTimeOffset.UtcNow.AddHours(72), // 3 days
Role = AuthorizationRole.DRIVER
};
var auth = await _client.CreateAuthorizationAsync(request);
Console.WriteLine($"Authorization {auth.AuthorizationId} created");
Console.WriteLine($"Expires at: {auth.ExpiresAt:yyyy-MM-dd HH:mm}");
return auth.AuthorizationId;
}
Wait for Authorization Release
public async Task<AuthorizationDetails> CreateAndWaitForRelease(
CreateAuthorizationRequest request)
{
// Create authorization
var auth = await _client.CreateAuthorizationAsync(request);
Console.WriteLine($"Authorization {auth.AuthorizationId} created with status {auth.Status}");
// Wait for authorization to be released
var options = new WaitOptions
{
PollingIntervalSeconds = 5,
TimeoutSeconds = 300,
CustomPredicate = a => a.Status == AuthorizationStatus.RELEASED
};
var details = await _client.WaitForAuthorizationAsync(
auth.AuthorizationId,
options);
Console.WriteLine($"Authorization {details.AuthorizationId} released");
return details;
}
Monitor Authorization Status
public async Task MonitorAuthorization(string authorizationId)
{
while (true)
{
var details = await _client.GetAuthorizationAsync(authorizationId);
Console.WriteLine($"Status: {details.Status}");
if (details.Status == AuthorizationStatus.RELEASED ||
details.Status == AuthorizationStatus.CANCELLED ||
details.Status == AuthorizationStatus.EXPIRED ||
details.Status == AuthorizationStatus.REVOKED)
{
Console.WriteLine("Authorization reached terminal status");
break;
}
await Task.Delay(TimeSpan.FromSeconds(10));
}
}
Error Handling Pattern
using CoxAuto.Vecu.CustodySdk.Exceptions;
public async Task<AuthorizationResponse?> CreateAuthorizationWithRetry(
CreateAuthorizationRequest request,
int maxRetries = 3)
{
for (int attempt = 1; attempt <= maxRetries; attempt++)
{
try
{
return await _client.CreateAuthorizationAsync(request);
}
catch (DuplicateAuthorizationException ex)
{
// Authorization already exists - not retryable
Console.WriteLine($"Duplicate authorization: {ex.Message}");
throw;
}
catch (VehicleNotReleasableException ex)
{
// Vehicle not releasable - not retryable
Console.WriteLine($"Vehicle not releasable: {ex.Message}");
throw;
}
catch (RateLimitException ex)
{
// Rate limited - wait and retry
if (attempt < maxRetries)
{
var delay = ex.RetryAfter ?? TimeSpan.FromSeconds(30);
Console.WriteLine($"Rate limited, retrying after {delay}");
await Task.Delay(delay);
}
}
catch (NetworkException ex)
{
// Network error - retry with backoff
if (attempt < maxRetries)
{
var delay = TimeSpan.FromSeconds(Math.Pow(2, attempt));
Console.WriteLine($"Network error, retrying in {delay}");
await Task.Delay(delay);
}
}
}
return null;
}
Multi-Driver Exception Hierarchy (ADR-042)
Multi-driver operations introduce additional typed exceptions. Most
inherit from intermediate base classes — e.g.,
MultiDriverModeRequiredException is a subclass of
InvalidConfigurationException, mirroring the existing
InvalidOriginAddressException precedent.
| Exception | HTTP | Server error code | Raised when |
|---|---|---|---|
MultiDriverModeRequiredException | 400 | MULTI_DRIVER_MODE_REQUIRED | Caller's client is in single_driver mode but the request used a multi-driver endpoint or supplied AuthorizedDrivers. |
EmptyDriverListException | 400 | EMPTY_DRIVER_LIST | UpdateAuthorizationDriversAsync net effect would leave zero drivers. |
DriverNotOnListException | 400 | DRIVER_NOT_ON_LIST | AssignAuthorizationToDriverAsync requested a driver not on the authorization's AuthorizedDrivers list. |
NotAuthorizationOwnerException | 403 | NOT_AUTHORIZATION_OWNER | Caller's client_id does not own the authorization (cross-tenant attempt). |
AuthorizationAlreadyAssignedException | 409 | AUTHORIZATION_ALREADY_ASSIGNED | UpdateAuthorizationDriversAsync against an ASSIGNED authorization. |
AuthorizationAlreadyAssignedToDifferentDriverException | 409 | AUTHORIZATION_ALREADY_ASSIGNED_TO_DIFFERENT_DRIVER | AssignAuthorizationToDriverAsync requested a different driver on an already-ASSIGNED authorization. |
AuthorizationNotAssignableException | 409 | AUTHORIZATION_NOT_ASSIGNABLE | AssignAuthorizationToDriverAsync against a terminal-status authorization. |
StaleETagException | 412 | STALE_ETAG | Version argument does not match the authorization's current version. Body carries CurrentVersion for retry. |
DriverLimitExceededException | 429 | DRIVER_LIMIT_EXCEEDED | Multi-driver POST or UpdateAuthorizationDriversAsync add would exceed per-client driver cap. |
PoolFanOutException | 503 | POOL_FANOUT_FAILURE | Multi-driver POST pool fan-out failed and was rolled back. Caller can retry the entire POST. |
AssignPartialFailureException | 503 | ASSIGN_PARTIAL_FAILURE | AssignAuthorizationToDriverAsync succeeded at the authorization level but credential revocation completed only partially. |
RetryExhaustedException | n/a | (SDK-side) | AddOrCreateDriverAsync exhausted its ETag-mismatch retry budget. Caller should back off and retry. |
// Catch the parent class to handle the whole config-error family at once
try
{
await _client.UpdateAuthorizationDriversAsync(authorizationId, request);
}
catch (InvalidConfigurationException ex)
{
// Handles MultiDriverModeRequiredException + any future config errors
Console.WriteLine($"Configuration issue: {ex.Message}");
}
Next Steps
- Multi-Driver Integration Guide - End-to-end flows for
multi_drivermode - Multi-Driver Migration Guide - Moving from exception-mode to strict-only uniqueness
- Releasability API - Check vehicle releasability
- Error Handling - Exception types and handling
- Testing - Mock client for testing