SAP CPQ

Creating Quotes Programmatically with the SAP CPQ Quote 2.0 API

Software developer reviewing code on two monitors in an office

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.

System administrator working at a workstation inside a data center

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. $top is capped there on the quote list, and exceeding it returns HTTP 400 with code 112000 and the message “The maximum of $top is 1000 “, trailing space included. Page with $skip rather 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 code 116000, 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.

Developer reading source code on screen in an open plan office

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.

Frequently Asked Questions

Which API should I use for SAP CPQ Quote 2.0?
Use the REST endpoints under /api/v1/quotes. SAP states that these were developed for the new quote engine and are not supported in the old one. The older REST endpoints still answer on a Quote 2.0 tenant, but SAP does not recommend them, and a long list of SOAP methods, including NewQuote and CreateNewQuoteAndGetQuoteData, stops working entirely.
Can I fetch the quotes waiting for approval through the standard API?
Not as a single list. The two documented approval endpoints are both scoped to one 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, filter the quote list by status with $filter=statusid eq '{id}', taking the status id from your own workflow configuration.
How do I update a quote custom field through the API in CPQ 2.0?
At quote level, send a PATCH to /api/v1/quotes/{quoteId} with the custom field as a top level key in the JSON body, alongside standard fields. At item level, use PATCH /api/v1/quotes/{quoteId}/items/{itemId}. In both cases the field has to be editable before the request is sent, or the value is rejected.
Why is my custom field empty in the on quote creation event?
Because the event fires before the field exists. Creating a quote through the REST API creates an empty quote with only the essentials set, which SAP lists as market, status, effective date, owner id and origin. Custom fields are written after the on quote creation event has been raised. Move any logic that reads custom field values to a later event.
Why is my item custom field ignored when I pass it in the POST body?
This is expected behaviour according to SAP: it is not possible to update a custom field through the Quote Items API. Sending a CustomFields array inside POST /api/v1/quotes/{quoteId}/items creates the item and silently drops the field. The documented route is two calls, create the item first, then set the field with PATCH on that item.
Why does the Customer API say it is unavailable?
Because Business Partners is switched on. In Quote 2.0, Customers are obsolete and replaced by Involved Parties, and when the Business Partners feature is enabled the Customer API returns the message that it is unavailable. SAP describes this as by design rather than a limitation: it is one model or the other, so the Business Partner calls are what you use.
How do I submit a quote for approval through the API?
Submit for Approval is an action, not its own endpoint. Post to /api/v1/quotes/{quoteId}/actions/{actionId}/invoke with action 25, and include a body listing the approval rules, each carrying Rule for the rule id, Approvers for the user ids and Comment for the rule comment. Approve is 26, Reject is 27 and Retract approval is 53. Confirm the numbers on your own tenant with GET /api/v1/quotes/{quoteId}/actions.
Can I set the quote id myself when creating a quote from a CRM?
No. The quote id in Quote 2.0 is not modifiable, which surprises teams pushing quotes in from SAP Sales Cloud and expecting to carry the CRM identifier across. Put that identifier in ExternalId instead. Since release 2608 the quote list can be filtered by externalId and opportunityId, so finding the quote again by the key your CRM knows is now practical.