Blog
Creating Quotes Programmatically with the SAP CPQ Quote 2.0 API
Search results for the SAP CPQ Quote 2.0 API lead to the SOAP interface built for the old quote engine, while the real specification sits behind a sign in. This is what the calls look like in practice, and where integrations built on them break.
What you will learn
- Which endpoints belong to Quote 2.0 and which SOAP methods stop working
- Why token lifetimes and session reuse decide whether a batch job survives
- Why creating a quote is a sequence of calls, not one
- How custom fields have to be written, and when a 200 OK means nothing happened
- How Submit for Approval works as an action, and what the approval endpoints cannot do
- What changed across releases 2602, 2605 and 2608
Search for the SAP CPQ Quote 2.0 API and the most authoritative page Google hands back is SAP’s own documentation for Create New Quote and Get Quote Data. It has a tidy table of input parameters, an XML sample, username and password authentication. It is also the SOAP interface built for the old quote engine, and nothing on that page says so. Build against it on a Quote 2.0 tenant and you find out later, from a response body rather than from the docs.
The specification you want sits on the SAP Business Accelerator Hub behind a sign in, invisible to anyone arriving from a search engine. What is left in public is a few concept pages and a Swagger file most people do not know exists. So I have written down the part that never gets published: what the calls really are, and where integrations built on them quietly break.
What the SAP CPQ Quote 2.0 API is, and what it is not
There is a naming problem to clear up first. SAP does not ship a product called the Quote 2.0 API. What exists is a family of REST endpoints under /api/v1/quotes, and SAP’s documentation is blunt about their scope: the REST APIs developed for the new quote engine are not supported in the old one. Call any of them on a Quote 1.0 tenant and the response is a single sentence: “This action is only supported for Quote 2.0.” That is the whole error. No hint about which interface you should have used instead.
Running the other direction, a long list of SOAP methods stops working when you move to the new engine, including NewQuote, CreateNewQuoteAndGetQuoteData, SearchQuotes, SetCartProperties, AddItemsData, GetQuoteData and GetItemAttributes. SAP’s position on the remainder is softer but clear enough: the older REST endpoints still answer on Quote 2.0, but their usage is not recommended. Anyone planning a setup where the quoting engine is driven entirely from outside the interface should treat that as a migration item, not a footnote.
The public Swagger file is the one reference that lists the real operations of the SAP CPQ Quote 2.0 API, and it lives on your own tenant at /webapihelp/index. SAP had to publish a knowledge base article explaining where to find it, which tells you how discoverable it is. It covers the quote header, items, revisions, comments, documents, attachments, involved parties, quote tables, approvals and actions.
Authentication, tokens and the session trap
Tokens come from /oauth2/token, posted as x-www-form-urlencoded. Two numbers matter more than the rest and both are easy to miss: an access token obtained through the password grant is valid for five minutes, and the refresh token lasts twenty and can be used exactly once before it becomes invalid. Plenty of nightly jobs assume a token survives the run. It does not.
Basic authentication still works on many tenants, but it is on the way out. The switch is an application parameter under Setup, General, Application Parameters, named Enable Basic Authentication For API, and setting it to FALSE stops Basic auth for REST, SOAP and Custom APIs at once. SAP says plainly that it will soon be phased out. If that parameter is still TRUE on your tenant, someone will eventually flip it, and every integration holding a stored password stops the same afternoon.
Then there is the failure that looks like a credentials problem and is not. A support article documents the message “Invalid user credentials or domain supplied.” appearing on middleware calls, with a cause that has nothing to do with the credentials: excessive session creation causes stability issues. The fix is to reuse one session across multiple calls rather than authenticating per request. Batch jobs written the obvious way, one login per record, hit this reliably, and the message sends everyone off to check passwords first. I have lost an afternoon to it. This is the kind of thing that surfaces during integration work on a tenant that already has traffic on it, long after the proof of concept passed.

Creating a quote is never a single call
The mental model most people bring to the SAP CPQ Quote 2.0 API is that POST /api/v1/quotes takes a payload and returns a finished quote. It does not, and understanding why explains half the bugs in this area.
The empty quote and the event that fires too early
SAP describes the sequence in a knowledge base article about custom fields. Creating a quote through the REST API creates an empty quote, exactly as the interface and IronPython do, and then populates it with everything else: custom fields, involved parties, items, quote table data. The on quote creation event fires against that empty shell, when only the essentials are set. SAP lists them: market, status, effective date, owner id, origin. Custom fields are written after the event has been raised.
The practical consequence is a script that logs nothing. Put Log.Info("Custom Field: {}".format(context.Quote.GetCustomField("Custom Field Name").Value)) on the creation event and you get an empty value, every time, with no error to investigate. SAP has this recorded as a filed bug rather than intended behaviour, which is worth knowing before you spend a day on it. Logic that depends on custom field values belongs on a later event, not on creation.
Custom fields need a second request
The item level version of the same problem is sharper, because the call succeeds. Send a CustomFields array inside POST /api/v1/quotes/{quoteId}/items, the way the quote header payload accepts one, and the item is created and the custom field is ignored. SAP’s answer is that this is expected behaviour: it is not possible to update a custom field through the Quote Items API. The documented route is two calls, create the item with POST, then set the field with PATCH /api/v1/quotes/{quoteId}/items/{itemId}.
There is a second, nastier variant. Custom fields passed in the body of /api/v1/quotes/{quoteId}/actions/{actionId}/invoke are not written, while the status comes back 200 OK and the action itself runs. SAP has that one filed as a bug too. If you are diffing expected against actual after a run, a silent 200 is the worst possible outcome, and it is the one you get.
One more trap catches anyone porting a Quote 1.0 integration. In Quote 2.0, Customers are obsolete and replaced by Involved Parties. On a tenant with the Business Partners feature switched on, the Customer API answers “API is unavailable because Business Partners feature is Enabled”, and SAP’s framing is that it is one or the other, by design, not a limitation. Cleaning this up across inherited scripts is ordinary work on a configuration that has drifted from its original design, but it is rarely scoped, because nothing fails until the call runs.
Approvals and limits, where integrations break
Approvals are where I field the most API questions, and the reason is structural: the thing people want to call is not an endpoint.
Submitting for approval is an action, not an endpoint
Quote transitions run through POST /api/v1/quotes/{quoteId}/actions/{actionId}/invoke, with the action identified by a number. Submit for Approval is 25, and it needs a body listing the approval rules, each with Rule for the rule id, Approvers for the user ids and Comment for the rule comment. Approve Quote is 26 and Reject Quote is 27, both taking a list of ApproversResponsibilities. Retract approval is 53 and wants only a Comment. Resolve those numbers on your own tenant with GET /api/v1/quotes/{quoteId}/actions rather than trusting a list from elsewhere. Not everything is exposed this way: reconfigure, save and reprice are among the actions the API does not support.
An approval chain that has to be driven through the API is usually a sign that the approval path itself was designed around the wrong bottleneck, and rebuilding it in the workflow costs less than wrapping it in code.
Now the question people actually search for: can you fetch the quotes waiting for approval through the standard API. Not the way you want. Two endpoints are documented and both are scoped to a single quote. /approvals returns the approval rules broken by the logged in user, and /approvals/responsibilities returns the ids of broken rules that user needs to approve or reject. For a list across quotes, the only supported route is filtering by status, GET /api/v1/quotes?$filter=statusid eq '{id}', with the status id taken from your own workflow configuration. The Waiting for Approval tab is populated by SAP’s User Interface API, and SAP states that this layer is not intended for administrators but for the system to use. Building on it means maintaining something SAP has not promised to keep stable. Approvers are named in the Submit for Approval payload itself, but there is no documented API for assigning one to a quote that is already in flight, a question that comes up constantly and has no answer beyond configuring approver selection logic.
Errors that do not say what they mean
Two failures are worth memorising because the message points away from the cause.
- Paging stops at 1000.
$topis capped there on the quote list, and exceeding it returns HTTP 400 with code112000and the message “The maximum of $top is 1000 “, trailing space included. Page with$skiprather than trying to raise the ceiling. - A harmless filter can return a SQL error. Calling
/api/v1/quotes?$filter=quotenumber eq 'QuoteNumber'can come back as code116000, internal server error, carrying “Incorrect syntax near the keyword ‘for’.” and a complaint about the FETCH statement. The cause is not your filter. It is a visibility rule created through script or API that stored a quote status name where the database expects an id, and the exception surfaces when quote visibility is evaluated.
Alongside these, the published limitations for the current release are worth reading once before design rather than after: GET query strings cap at 2048 characters, JSON strings at roughly 4MB, Custom API payloads at 2MB, and calls SAP sends out to an external system time out at 100 seconds.

What changed between 2602 and 2608
The API surface has moved in every recent release, and two of those changes alter code you may already have running.
Release 2602 taught the Quotes POST method to process configuration details, so configuration data can travel with quote creation in one call instead of the follow up requests it used to need. The same release improved error handling, so a failed creation returns a detailed message when data is invalid or missing. Release 2605 added QuoteItemBulkUpdate, a REST API for updating multiple items and their item level fields in a single request, with pricing recalculation triggered automatically and the whole thing processed as one operation. For anyone looping PATCH over items today, that is the single change from that release most worth acting on.
Release 2608 extended the quote list filters with opportunityId and externalId, which finally makes it practical to find a quote by the key your CRM knows it by. The same release carries a breaking change flagged as action required: the quote list response no longer includes the WorkflowTransition property, because SAP is deprecating direct exposure of that internal object and pointing integrations at explicit properties on the data transfer objects instead. If anything in your middleware reads WorkflowTransition, it stops reading it, so the release by release record of what actually shipped is worth a pass before any upgrade window.
And one limit that will not change soon: the quote id in Quote 2.0 is not modifiable. Teams pushing quotes in from SAP Sales Cloud regularly try to create a quote carrying the CRM’s own identifier and cannot. The identifier belongs in ExternalId, which, conveniently, is now filterable.
Three things to take away. The endpoints you want sit under /api/v1/quotes, while the most visible documentation in search results describes the interface you should not use. Quote creation is a sequence, not a call, so anything reading custom fields has to wait for them to arrive. And the errors here are unusually bad at naming their own cause, which makes the knowledge base worth searching before your own code. If you want a second opinion on where an integration will bend, that is the kind of review we do on running systems.