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 line | series | Notes |
|---|---|---|
| Prime | Prime |
Direct sale. |
| Plus | Plus |
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.
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.
| Credential | Identifies | Travels as |
|---|---|---|
client_id | your integration | request header |
client_secret | your integration | request header |
| username | the seller quotes are written against | request body |
| password | the seller quotes are written against | request 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/dealersmeans 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.
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
- POST/v1/authenticate Returns the bearer token.
-
GET/v1/quote/vehicle-configurations
Pick a
vehicleConfigurationId. -
GET/v1/quote/dealers
Pick a
dealerId. Plus only -
GET/v1/quote/lienholders
Pick a
lienHolderId. Optional - Leases and finances -
POST/v1/quote
Returns
applicationIdand the priced options. -
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 -
POST/v1/bind
Returns
policyNumberanddocumentDownloadId. - 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
purchaseMethodis a lease or a finance. SendinglienHolderIdis 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
EffectiveDateBackDateandEffectiveDateFutureotherwise). 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
applicationIdback. 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: falsemust be present. paymentTermInYearsmust 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.
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
applicationIdreturns the policy the first attempt created:200, samepolicyNumber, samedocumentDownloadId. 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,cardPaymentorfinancingLetterDownloadIdvalues, 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.
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.
| Code | Meaning |
|---|---|
REF | Referral |
FIN | Agency finance |
PW | Agency-billed PW |
PFF | Monthly financing through Optiom's lender. See Financing. |
CC | Credit 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.
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:
institutionNumberis 3 digits,transitNumber5,accountNumber7 to 12.downPaymentis at most two decimals and less than the premium.-
firstInstallmentDatefalls between the day after the policy effective date and a month later. The error names the window. loanTermInMonthsis 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.
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.
| Step | Where it runs | What you write |
|---|---|---|
| 1. Start | Your server | A call to us. Returns a ticket. |
| 2. Render | The customer's browser | A script tag, an empty div, and seven lines of JavaScript, copied below. |
| 3. Confirm | Your server | A call to us that returns whether the card was accepted. |
| 4. Bind | Your server | The 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:
| Status | What to do |
|---|---|
Authorized | Bind with this paymentId. |
Pending | The customer has not finished. Ask again. |
Declined | Show the message and start a new card payment. |
Cancelled | The 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.
| Code | What to do |
|---|---|
CardPaymentUnavailable | Retry the bind with the same payment id. A charge that did go through is reused, never repeated. |
BindIncomplete | Retry the bind with the same payment id. Nothing was charged. |
CardPaymentNotAuthorized | Confirm first. If the card was declined, start a new payment. |
CardPaymentDeclined | The issuer refused the charge. Start a new payment. |
CardPaymentExpired | The authorization lapsed. Start a new payment. |
CardPaymentAmountMismatch | The price changed since the customer paid. Start a new payment for the current price. |
CardPaymentAlreadyUsed | Do not retry. Contact Optiom with the payment id. |
BindFailedAfterCardCapture | Do 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.
codeis stable. Branch on it.fieldnames the request property, for attaching the message to a form field. Empty when the error applies to the request as a whole.messageis displayable text. Its wording is not part of the contract, so never match on it.
| Status | Meaning |
|---|---|
400 | The request produced no result. Empty body. |
401 | Missing, expired or invalid token. |
422 | Understood and refused. Nearly every business rule lands here. |
429 | Rate limited. Honour Retry-After. |
500 | Our fault. code is InternalError; quote the timestamp to support. |
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
| Code | Where | What to do |
|---|---|---|
BindInProgress | bind | Wait and retry. Not a failure. |
QuoteAlreadyBound | bind | The quote is spent. Do not retry; the policy exists. |
SeriesNotEnabledForSeller | quote | Configuration. Raise with Optiom. |
DealerNotLinkedToSeller | quote | The dealer is not in /quote/dealers for this seller. |
DealerIdRequiredDealerContactRequired | quote (Plus) | Both are mandatory on a Plus quote. |
LienHolderRequired | quote | The purchase method is a lease or finance. |
EffectiveDateBackDateEffectiveDateFuture | quote | Outside the today-to-30-days window. |
VehicleIneligible | quote | The vehicle does not qualify. Not retryable. |
SelectedProductsMismatch | bind | A product does not belong to the chosen coverage category. |
PaymentTermBelowSeriesMinimum | bind | Plus sells from two years. |
FinancingDetailsRequiredFinancingNotApplicable | bind | The financing block belongs with PFF and only with PFF. |
FinancingNotAvailableForCoverageCategory | financing, bind | Not offered on LimitedIndemnity. |
LoanTermNotAvailable | bind | Use a loanTermInMonths the options call returned. |
DownPaymentInvalidFirstInstallmentDateOutOfRange | financing, bind | The message states the limit or the window. |
InvalidBankInstitutionInvalidBankTransitInvalidBankAccount | bind | Check the digit lengths. |
FinancingUnavailable | financing, bind | The lender could not be reached. Nothing was created; retry. |
FinancingDeclined | financing, bind | The lender refused the plan. Do not retry it unchanged. |
PolicyNumberInUseByLender | bind | Optiom is notified. Contact us; a retry cannot succeed. |
BindFailedAfterFinancing | bind | The loan was written and reversed, no policy exists. Contact Optiom with the loan number. |
ChargeDateOutOfRange | card | Charge on a day between today and the effective date. |
PaymentIdRequired | card | Send the payment id from the start call. |
CardPaymentRequired | bind | CC needs a cardPayment block. |
CardPaymentNotApplicable | bind | Only CC takes a cardPayment block. |
CardPaymentUnavailable | card, bind | Moneris could not be reached. Retry the same call. |
CardPaymentNotFound | card, bind | Unknown payment, or not for this quote. |
CardPaymentNotAuthorized | bind | Confirm the payment first; start a new one if the card was declined. |
CardPaymentDeclined | bind | The issuer refused the charge. Start a new payment. |
CardPaymentExpired | bind | The authorization lapsed. Start a new payment. |
CardPaymentAmountMismatch | bind | The price changed. Start a new payment for the current price. |
CardPaymentAlreadyUsed | bind | Already charged. Contact Optiom; a retry cannot succeed. |
BindFailedAfterCardCapture | bind | The card was charged and is being reversed, no policy exists. Contact Optiom with the payment id. |
DocumentNotFound | document | Unknown 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.