Optiom Inc

Portal API  ·  Version 1

Partner Integration Guide

A VIN goes in, and a bound policy and its declaration document come out, in a handful of calls. This guide covers the sequence and the reasoning; the OpenAPI document covers every field.

Reviewed 2026-08-28 Spec /swagger/v1/swagger.json This page /docs Status Pre-launch

What this API does

It sells Optiom vehicle protection products. A partner decodes a VIN, prices the coverage, binds the policy and downloads the declaration, which is the same path an Optiom seller takes through the Portal web application, collapsed into four calls. A customer paying monthly adds one more, to price the financing before the bind writes the loan.

Product lineseriesNotes
PrimePrime Direct sale.
PlusPlus Dealer sale. Needs a dealer id and a dealer contact.

Not available yet

Plan around these rather than for them.

  • Post-sale servicing. There is no endpoint to retrieve a policy, endorse it, cancel it or check a claim. Everything after bind goes through Optiom staff.
Store this yourself

You cannot fetch a quote or a policy back by id. Persist the applicationId, the policyNumber and the declaration document at the moment you receive them.

Before your first call

Optiom issues four credentials at onboarding, in two pairs.

CredentialIdentifiesTravels as
client_idyour integrationrequest header
client_secretyour integrationrequest header
usernamethe seller quotes are written againstrequest body
passwordthe seller quotes are written againstrequest body

Confirm three more things at onboarding, because each one turns into a runtime rejection if it is wrong.

  • Which series your seller is entitled to. Quoting a series the seller cannot sell returns SeriesNotEnabledForSeller.
  • Whether your seller requires a producer code. Some do. If yours does and you omit it, every quote fails.
  • For Plus, that dealer links exist. An empty list from GET /v1/quote/dealers means the seller has no active dealer links and cannot sell Plus at all.

Versioning and base URL

Every route is versioned: /v1/.... There is no unversioned form. Calling /quote instead of /v1/quote fails rather than defaulting. An integration is always pinned to a known contract.

/healthcheck, /swagger and /docs (this page) are the exceptions and stay unversioned.

Authentication

POST /v1/authenticate
client_id: {your client id}
client_secret: {your client secret}

{ "username": "...", "password": "..." }

Returns token, expiryTime (UTC) and expiryMinutes. Send it on every other call as Authorization: Bearer {token}.

Tokens last 60 minutes by default. Cache the token and reuse it until it expires. Reauthenticating per request works, but spends your rate-limit allowance on it.

A wrong credential, any of the four, returns 401 with no body. Which one was wrong is not disclosed.

This contract is provisional

Today it is a hybrid: two credentials in headers, two in the body. A later version replaces it with a standard OAuth2 client-credentials grant. Keep token acquisition behind one function in your client and the change stays contained to it.

The sequence

  1. POST/v1/authenticate Returns the bearer token.
  2. GET/v1/quote/vehicle-configurations Pick a vehicleConfigurationId.
  3. GET/v1/quote/dealers Pick a dealerId. Plus only
  4. GET/v1/quote/lienholders Pick a lienHolderId. Optional - Leases and finances
  5. POST/v1/quote Returns applicationId and the priced options.
  6. POST/v1/quote/financing
    POST/v1/payment/card
    Set up the payment, where the method needs it. Financed sales price the monthly options; card sales open a checkout and then confirm it. Agency-billed sales need nothing here. Optional - Depends on payment method
  7. POST/v1/bind Returns policyNumber and documentDownloadId.
  8. GET/v1/document/{downloadId} Returns the declaration PDF.

Resolve the vehicle

A VIN identifies a model and year but rarely a single trim, so vehicle-configurations returns every configuration it decodes to. Pick the one appropriate to your customer's vehicle and pass its id. If the return is a single result it still has to be chosen explicitly.

Send the same odometer, effectiveDate and purchaseMethod here and on the quote. Eligibility is judged against them, not against the VIN alone, so an empty vehicles list means the VIN decoded but nothing was eligible under those inputs.

Resolve the lookups

  • Dealers are scoped to your seller and required for Plus. The set changes rarely, so you may cache it rather than calling it per quote, being aware that new dealers were added to your network would need a refresh.
  • Lienholders need a search term of three characters or more, and are required when purchaseMethod is a lease or a finance. Sending lienHolderId is sufficient; the name and postal code resolve from it.

Quote

POST /v1/quote Contains everything the initial quote depends on, and returns all that is available for your selection.

  • Effective date must be today or later, and no more than 30 days ahead (validates on error codes EffectiveDateBackDate and EffectiveDateFuture otherwise). It drives pricing, so a quote taken today for a later date will hold prices as of today.
  • Province is not an input. It is derived from your seller, and the applicant address must be in it.
  • Revising a quote means sending the applicationId back. A revision reprices from scratch, so send the complete request again, not just the changed fields.

Read the pricing matrix

The response carries one list per coverage family.

replacementOptions      -> coverageCategory = Replacement
limitedIndemnityOptions -> coverageCategory = LimitedIndemnity

Each list is a set of terms; each term holds the products priced at that term.

replacementOptions[]
  paymentTermInYears: 4
  coverage[]
    productType:  "..."    <- goes into selectedProducts
    totalWithTax: 1234.56  <- what the customer pays for this product
    isOptional:   false    <- if false, it MUST be in selectedProducts

The customer's total for a term is the sum of totalWithTax across the products you select from it. A product's own coverageTermInYears can differ from the paymentTermInYears it sits under, so show the product's value when displaying coverage length.

Bind takes one coordinate from that matrix: one coverage category, one term, and the product codes chosen within them.

Bind

POST /v1/bind
{
  "applicationId":      "...",
  "coverageCategory":   "Replacement",
  "paymentTermInYears": 4,
  "selectedProducts":   ["...", "..."],
  "paymentMethod":      "REF",
  "paymentReference":   "your reference"
}

Rules the API enforces:

  • Every product must belong to the coverage category you named (SelectedProductsMismatch).
  • Every product with isOptional: false must be present.
  • paymentTermInYears must be a term that appeared in that category, not an arbitrary number.
  • Plus sells from a two-year minimum (PaymentTermBelowSeriesMinimum); Prime from one.

To sell it on monthly payments, send paymentMethod: "PFF" with a financing block instead of a paymentReference. See Financing.

Point of no return

Bind creates the policy, charges, and generates documents. There is no partner-facing reversal. Cancellation goes through Optiom.

Download the documents

GET /v1/document/{documentDownloadId} returns the file itself rather than a JSON envelope, normally application/pdf, with the filename in Content-Disposition. It is available immediately after bind, stays available, and can be fetched more than once. Each fetch is recorded as a reprint.

A financed bind produces two: the declaration under documentDownloadId and the lender's notice of acceptance under financingLetterDownloadId. A card bind charged during the bind also produces the receipt, under cardPayment.receiptDownloadId. All of them come back from this same endpoint.

A document belonging to another seller and a document that does not exist both return the same DocumentNotFound. The id space is not probeable.

Retrying a bind

  • A repeated bind of the same applicationId returns the policy the first attempt created: 200, same policyNumber, same documentDownloadId. It does not create a second policy.
  • A bind still in flight returns 422 BindInProgress. Wait and retry.
  • That replay does not repeat the financing, cardPayment or financingLetterDownloadId values, even for a policy that was financed or paid by card. A null there on a retry means the response is a replay, not that the loan or the charge is missing. Keep the values from the original response.
The rule

After a timeout, a dropped connection, or any response you did not read, retry the bind. Do not report it as failed.

The first attempt may well have succeeded, and the retry is what tells you. Treat a bind as failed only on a definite non-retryable error. Never re-quote and re-bind to start clean after an ambiguous bind. That is the one path that produces two policies for one sale.

Payment methods

Three methods settle before the bind, one finances the premium, and one charges a card.

CodeMeaning
REFReferral
FINAgency finance
PWAgency-billed PW
PFFMonthly financing through Optiom's lender. See Financing.
CCCredit card, entered by the customer on Moneris. See Card payments.

paymentReference is your own reference for the payment already taken, such as a cheque number or a receipt id. It is stored against the policy for reconciliation and is not validated against any provider. Leave it empty for PFF and CC: the loan number, or the card approval code, becomes the reference instead.

Anything else returns PaymentMethodNotSupported. A method your seller is not configured for returns PaymentMethodNotAvailableForSeller, which is a configuration issue to raise with Optiom rather than something to retry.

Financing

Financing spreads the premium over monthly pre-authorised debits from the customer's chequing account. It is available on Prime and Plus replacement coverage, and your seller has to be set up for it.

1. Price the options

Send the same coverage choice you intend to bind, plus the down payment and the date the customer wants the first installment taken. Nothing is created, so call it as often as you need while the customer settles on a plan.

POST /v1/quote/financing
{
  "applicationId":        "...",
  "coverageCategory":     "Replacement",
  "paymentTermInYears":   4,
  "selectedProducts":     ["...", "..."],
  "downPayment":          250.00,
  "firstInstallmentDate": "2026-06-15"
}

Each entry in options is a loan the customer could take, carrying the installment amount, the finance charge, the application fee, the APR and the total payable. Take loanTermInMonths from the one they choose.

Indicative

These figures are for showing the customer. The lender prices the contract when it is created, so the binding numbers are the ones on the bind response.

2. Bind with the plan

POST /v1/bind
{
  "applicationId":      "...",
  "coverageCategory":   "Replacement",
  "paymentTermInYears": 4,
  "selectedProducts":   ["...", "..."],
  "paymentMethod":      "PFF",
  "financing": {
    "loanTermInMonths":     24,
    "downPayment":          250.00,
    "firstInstallmentDate": "2026-06-15",
    "bankAccount": {
      "institutionNumber": "004",
      "transitNumber":     "12345",
      "accountNumber":     "1234567",
      "accountHolderName": "Jane Doe"
    }
  }
}

Rules the API enforces before it contacts the lender:

  • institutionNumber is 3 digits, transitNumber 5, accountNumber 7 to 12.
  • downPayment is at most two decimals and less than the premium.
  • firstInstallmentDate falls between the day after the policy effective date and a month later. The error names the window.
  • loanTermInMonths is one the options call offered for that policy term.

A successful bind returns financing with the loan number and the contract figures, and financingLetterDownloadId for the lender's notice of acceptance. Fetch it the same way as the declaration.

Do not retry a financed bind blindly

FinancingUnavailable means nothing was created at the lender: retry it. FinancingDeclined, PolicyNumberInUseByLender and BindFailedAfterFinancing all mean a retry cannot succeed. Contact Optiom, quoting the loan number when the error carries one.

The account details are passed to the lender and not stored by Optiom, and they are masked out of our request logs.

Card payments

The customer types their card into Moneris Checkout, a form Moneris hosts and renders inside your own page. The card number never reaches your servers or ours, which keeps both of us out of scope for handling it.

What you build

Four steps: three calls to us, and one place where you render Moneris's form. If you have integrated a payment provider before, this is the same shape as every other one. If you have not, there is less here than you might expect.

StepWhere it runsWhat you write
1. StartYour serverA call to us. Returns a ticket.
2. RenderThe customer's browserA script tag, an empty div, and seven lines of JavaScript, copied below.
3. ConfirmYour serverA call to us that returns whether the card was accepted.
4. BindYour serverThe usual bind call, carrying the payment id.

You do not need a Moneris account, a merchant agreement, or any relationship with Moneris. You are not handling card data, so this does not put you in PCI scope. The customer stays on your site throughout: nothing redirects anywhere.

Card payments need CC in your seller's configuration. Ask Optiom at onboarding whether yours has it.

Before you write any of this, you can check your credentials and your quote work. Open a payment, then paste the ticket into GET /card-checkout on this same host, which renders the checkout for you. It exists only on test environments and is there to get you a first successful payment before you build the browser step.

1. Start the payment

Send the same coverage selection you intend to bind, so the amount matches. Changing it afterwards invalidates the payment.

POST /v1/payment/card

{
  "applicationId": "3f1c…",
  "coverageCategory": "Replacement",
  "selectedProducts": ["TUQ"],
  "paymentTermInYears": 5
}
{
  "isValid": true,
  "paymentId": "8c2a…",
  "status": "Pending",
  "ticket": "1585G9G9GIKKGGGIGIOG09G9OGKGJFKFJFNjuit8g9",
  "amount": 0.00,
  "chargeDate": "2026-05-10",
  "checkoutJsUrl": "https://gatewayt.moneris.com/chktv2/js/chkt_v3.00.js",
  "environment": "qa",
  "expiresAt": "2026-04-30T18:30:00Z"
}

By default the card is charged on the policy effective date, which for a policy starting today means during the bind. Send chargeDate as today to take the money up front on a future-dated policy. A future charge date places no hold on the card and amount comes back as zero: the card is verified, saved, and charged on the day.

2. Render the checkout

Load the script and ticket from the response rather than hard-coding either.

<script src="{checkoutJsUrl}"></script>
<div id="monerisCheckout"></div>
<script>
  const checkout = new monerisCheckout();
  checkout.setMode("{environment}");
  checkout.setCheckoutDiv("monerisCheckout");
  checkout.setCallback("payment_complete", () => confirmWithOptiom());
  checkout.setCallback("cancel_transaction", () => startAgain());
  checkout.setCallback("error_event", () => confirmWithOptiom());
  checkout.startCheckout("{ticket}");
</script>

The browser callbacks are a hint, not an outcome. Only the confirm call below says whether the card was accepted, because only it asks Moneris. Treat the ticket as a secret: never log it, never put it in a URL, and never store it. It expires in about half an hour, after which start again.

3. Confirm

This step is required, not a check you can skip. We deliberately do not keep the ticket, so this call is the only moment we can ask Moneris what happened. Bind before it and the payment is still Pending, so the bind fails with CardPaymentNotAuthorized.

POST /v1/payment/card/confirm

{ "applicationId": "3f1c…", "paymentId": "8c2a…", "ticket": "1585G9G9…" }

Read status:

StatusWhat to do
AuthorizedBind with this paymentId.
PendingThe customer has not finished. Ask again.
DeclinedShow the message and start a new card payment.
CancelledThe customer closed the checkout. Start again.

Confirming is safe to repeat and charges nothing. A declined card leaves the quote untouched, so starting again costs nothing but another ticket.

4. Bind

POST /v1/bind

{
  "applicationId": "3f1c…",
  "paymentMethod": "CC",
  "cardPayment": { "paymentId": "8c2a…" },
  "paymentTermInYears": 5,
  "coverageCategory": "Replacement",
  "selectedProducts": ["TUQ"]
}

The response carries the card alongside the policy:

"cardPayment": {
  "paymentId": "8c2a…",
  "status": "Captured",
  "maskedCardNumber": "424242******4242",
  "cardType": "Visa",
  "amountCharged": 900.00,
  "chargeDate": "2026-04-30",
  "authorizationNumber": "09968E",
  "transactionDate": "2026-04-30T18:05:22Z",
  "receiptDownloadId": "5d41…"
}

status is Captured when the money was taken during the bind, and Scheduled when it is due on chargeDate. A scheduled charge reports amountCharged as zero and no approval code until the day it runs.

receiptDownloadId is the customer's credit card receipt, fetched from GET /v1/document/{downloadId} like any other document. It is the same receipt Optiom issues for a sale made in the Portal. A scheduled charge has nothing to receipt yet, so the field is absent until Optiom can supply one.

When a bind fails

What to do depends on the code, and guessing is expensive here.

CodeWhat to do
CardPaymentUnavailableRetry the bind with the same payment id. A charge that did go through is reused, never repeated.
BindIncompleteRetry the bind with the same payment id. Nothing was charged.
CardPaymentNotAuthorizedConfirm first. If the card was declined, start a new payment.
CardPaymentDeclinedThe issuer refused the charge. Start a new payment.
CardPaymentExpiredThe authorization lapsed. Start a new payment.
CardPaymentAmountMismatchThe price changed since the customer paid. Start a new payment for the current price.
CardPaymentAlreadyUsedDo not retry. Contact Optiom with the payment id.
BindFailedAfterCardCaptureDo not retry. The card was charged, the policy was not, and the charge is being reversed. Contact Optiom with the payment id.

Bind before the authorization lapses. expiresAt on the confirm response is the deadline. The hold releases itself afterwards and the customer is never charged, but the payment can no longer be bound.

Errors

Every failure returns the same envelope.

{
  "isValid": false,
  "errors": [
    { "code": "DealerNotLinkedToSeller", "field": "DealerId", "message": "..." }
  ]
}

Successful quote and bind responses carry isValid and errors too, so one branch handles both.

  • code is stable. Branch on it.
  • field names the request property, for attaching the message to a form field. Empty when the error applies to the request as a whole.
  • message is displayable text. Its wording is not part of the contract, so never match on it.
StatusMeaning
400The request produced no result. Empty body.
401Missing, expired or invalid token.
422Understood and refused. Nearly every business rule lands here.
429Rate limited. Honour Retry-After.
500Our fault. code is InternalError; quote the timestamp to support.
Expect codes you don't recognise

The list below covers the codes worth branching on. It is not the full set the API can return. Most codes originate in our quoting engine and can reach you unchanged, so a new validation rule there can produce one that is not documented here yet.

Treating code as a closed set can therefore throw on one it has never seen, and a fallback branch avoids that. What the branch shows is your call. Our message is written for a customer to read, though you may prefer your own wording for codes you have not mapped.

Codes worth handling specifically

CodeWhereWhat to do
BindInProgressbindWait and retry. Not a failure.
QuoteAlreadyBoundbindThe quote is spent. Do not retry; the policy exists.
SeriesNotEnabledForSellerquoteConfiguration. Raise with Optiom.
DealerNotLinkedToSellerquoteThe dealer is not in /quote/dealers for this seller.
DealerIdRequired
DealerContactRequired
quote (Plus)Both are mandatory on a Plus quote.
LienHolderRequiredquoteThe purchase method is a lease or finance.
EffectiveDateBackDate
EffectiveDateFuture
quoteOutside the today-to-30-days window.
VehicleIneligiblequoteThe vehicle does not qualify. Not retryable.
SelectedProductsMismatchbindA product does not belong to the chosen coverage category.
PaymentTermBelowSeriesMinimumbindPlus sells from two years.
FinancingDetailsRequired
FinancingNotApplicable
bindThe financing block belongs with PFF and only with PFF.
FinancingNotAvailableForCoverageCategoryfinancing, bindNot offered on LimitedIndemnity.
LoanTermNotAvailablebindUse a loanTermInMonths the options call returned.
DownPaymentInvalid
FirstInstallmentDateOutOfRange
financing, bindThe message states the limit or the window.
InvalidBankInstitution
InvalidBankTransit
InvalidBankAccount
bindCheck the digit lengths.
FinancingUnavailablefinancing, bindThe lender could not be reached. Nothing was created; retry.
FinancingDeclinedfinancing, bindThe lender refused the plan. Do not retry it unchanged.
PolicyNumberInUseByLenderbindOptiom is notified. Contact us; a retry cannot succeed.
BindFailedAfterFinancingbindThe loan was written and reversed, no policy exists. Contact Optiom with the loan number.
ChargeDateOutOfRangecardCharge on a day between today and the effective date.
PaymentIdRequiredcardSend the payment id from the start call.
CardPaymentRequiredbindCC needs a cardPayment block.
CardPaymentNotApplicablebindOnly CC takes a cardPayment block.
CardPaymentUnavailablecard, bindMoneris could not be reached. Retry the same call.
CardPaymentNotFoundcard, bindUnknown payment, or not for this quote.
CardPaymentNotAuthorizedbindConfirm the payment first; start a new one if the card was declined.
CardPaymentDeclinedbindThe issuer refused the charge. Start a new payment.
CardPaymentExpiredbindThe authorization lapsed. Start a new payment.
CardPaymentAmountMismatchbindThe price changed. Start a new payment for the current price.
CardPaymentAlreadyUsedbindAlready charged. Contact Optiom; a retry cannot succeed.
BindFailedAfterCardCapturebindThe card was charged and is being reversed, no policy exists. Contact Optiom with the payment id.
DocumentNotFounddocumentUnknown id, or not yours.

Rate limiting

Requests are throttled per partner, 300 per 60 seconds by default. A 429 carries Retry-After in seconds; honour it rather than retrying immediately.

The limiter guards against runaway loops. It is not a committed throughput, so agree one with Optiom rather than inferring it from the limit you observe.

Testing

Non-production environments expose the interactive spec at /swagger and this guide at /docs. Neither is served in production.

GET /v1/test-vin?year=2024 returns a real, unused VIN. Quoting twice against the same VIN trips duplicate-policy checks, so use this to get a fresh one per run rather than keeping a list. It is disabled in production and is not part of what you ship.

Getting help

Quote the policyNumber if you have one, otherwise the applicationId, plus the UTC timestamp of the call. Both are logged on our side and are enough to find the request.