# Create a Private Offer via API

## Overview

Create an Oracle Cloud Marketplace private offer programmatically with the Suger
[`CreateOffer`](https://doc.suger.io/api) endpoint. Suger creates the offer in OCI, attaches the
contract document, and sends it to the buyer — the same flow the console runs, driven from your own
system.

The call is **asynchronous**. Suger validates the request, persists a `DRAFT` offer, returns it
immediately, and then works with OCI in the background. The offer's real status arrives through the
hourly marketplace sync; poll the offer to follow it.

## Prerequisites

- An [Oracle Marketplace integration](/oracle-marketplace/integration) connected at the organization
  level.
- The product **synced into Suger** from your OCI listing — Suger resolves the listing OCID from the
  product's external ID, so a product created by hand will not work. See
  [List a Product](/oracle-marketplace/list-product).
- The buyer's **tenancy OCID**, or an existing Suger buyer record whose partner is `ORACLE`.
- A contract document (EULA) reachable over `https`, or already uploaded to Suger.
- An access token — see [OAuth App](/get-started/oauth-app).

## Endpoint

```
POST https://api.suger.cloud/org/{orgId}/offer
```

| | |
|---|---|
| **Operation ID** | `CreateOffer` |
| **Auth** | `Authorization: Bearer <access_token>` |
| **Body** | A `WorkloadOffer` object |
| **Success** | `200` with the created `WorkloadOffer` |

## Request fields

### Top level

| Field | Required | Notes |
|-------|----------|-------|
| `partner` | ✅ | `ORACLE` |
| `service` | ✅ | `MARKETPLACE` |
| `offerType` | ✅ | `PRIVATE`. Oracle supports no other type — `CPPO_OUT` and channel offers are rejected. |
| `productID` | ✅ | Suger product ID. Must be an Oracle-synced product carrying the listing OCID. |
| `name` | ✅ | Becomes the offer's `displayName` in OCI. Oracle rejects an empty name. |
| `buyerID` | ⬜ | A Suger buyer to link. Required only if you omit `info.oracleOffer.buyerTenancyOCID`; the buyer must belong to partner `ORACLE`. |

### `info`

| Field | Required | Notes |
|-------|----------|-------|
| `info.eulaUrl` | ✅ | The contract document. Either an `https` URL that Suger fetches server-side, or the key of a file you uploaded to Suger. **Plain `http` is rejected.** Must resolve to a **PDF of at most 1MB** (Oracle's per-attachment limit). |
| `info.additionalEulaUrls` | ⬜ | Further contract documents, same format and limits as `info.eulaUrl`. Each becomes its **own attachment** on the OCI offer (unlike AWS, where additional EULAs are merged into one document). |
| `info.currency` | ⬜ | ISO-4217 code. Defaults to `USD`. |
| `info.commits` | ⬜ | The offer's line items. Their total becomes the offer's `totalAmount` — see [Pricing](#pricing) below. |

### `info.oracleOffer`

Oracle-specific fields, an `OracleMarketplaceOffer` object.

| Field | Required | Notes |
|-------|----------|-------|
| `sellerPrimaryContact` | ✅ | Must include `email`. Oracle fails the send with `SELLER_INFORMATION_IS_NULL` without it. |
| `buyerPrimaryContact` | ✅ | Must include `email`. Oracle rejects the send without a buyer contact. |
| `buyerTenancyOCID` | ✅* | The buyer's OCI tenancy OCID. *Required unless you set `buyerID` to a linked Oracle buyer; when both are present, this field wins. |
| `buyerCompanyName` | ⬜ | Buyer's company name. |
| `description` | ⬜ | Offer description shown in OCI. |
| `duration` | ⬜ | ISO-8601 contract duration after the start date. Defaults to **`P1Y`** (one year). |
| `timeStartDate` | ⬜ | When the accepted offer becomes active. Defaults to **tomorrow**. |
| `timeAcceptBy` | ⬜ | Deadline for the buyer to accept. Defaults to **30 days out**. |

Contact objects take `firstName`, `lastName`, and `email`.

:::note
Fields such as `offerID`, `offerStatus`, `pricing`, `timeAccepted`, `timeOfferEnd`,
`resourceBundles`, and `resolvedProductIDs` are **read-only** — Suger populates them from OCI.
Sending them has no effect. In particular, `pricing.totalAmount` cannot be set directly: the
offer's amount comes exclusively from `info.commits` (see [Pricing](#pricing)).
:::

## Pricing

Oracle offers are sent to OCI with a single `ONE_TIME` billing cycle and one total amount. Suger
computes that total from `info.commits`:

```
totalAmount = round( Σ (commit.rate × commit.quantity) )
```

- A commit with no `quantity` counts as `1`.
- The sum is **rounded** to the nearest whole unit of currency, never truncated.
- Omit `info.commits` entirely and the offer is created with a total of `0`.

:::note
For Oracle this total is the whole of the offer's pricing — OCI has no dimension catalog and no
per-dimension rate card, so the individual commit lines are not sent to Oracle, only their sum. Usage
on the resulting entitlement is billed separately and is also reported as a monetary amount; see
[Usage Metering](/oracle-marketplace/usage-metering).
:::

## Example

The default shape below carries **no `info.commits`**, so the offer is sent with a **$0 total** —
the usage-only pattern, where consumption is billed separately via
[Usage Metering](/oracle-marketplace/usage-metering). To carry a contract amount instead, add
commit lines (see [Pricing](#pricing)):

```json
"commits": [{ "name": "Platform subscription", "rate": 50000, "quantity": 1 }]
```

```shell
curl -L -X POST 'https://api.suger.cloud/org/YOUR_ORG_ID/offer' \
  -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
  -H 'Content-Type: application/json' \
  --data-raw '{
    "partner": "ORACLE",
    "service": "MARKETPLACE",
    "offerType": "PRIVATE",
    "productID": "KVvfb0Vhv",
    "name": "Acme Corp - Annual Contract 2026",
    "info": {
      "currency": "USD",
      "eulaUrl": "https://acme.example.com/legal/eula-2026.pdf",
      "additionalEulaUrls": [
        "https://acme.example.com/legal/annex-a-2026.pdf"
      ],
      "oracleOffer": {
        "description": "Annual platform subscription for Acme Corp.",
        "duration": "P1Y",
        "buyerTenancyOCID": "ocid1.tenancy.oc1..aaaaaaaaexamplebuyertenancy",
        "buyerCompanyName": "Acme Corp",
        "buyerPrimaryContact": {
          "firstName": "Ada",
          "lastName": "Lovelace",
          "email": "ada@acme.example.com"
        },
        "sellerPrimaryContact": {
          "firstName": "Grace",
          "lastName": "Hopper",
          "email": "grace@yourcompany.com"
        }
      }
    }
  }'
```

The response is the persisted offer. Note `status` — it is the Suger record, not yet the OCI state:

```json
{
  "id": "FIwCbefqu",
  "organizationID": "YOUR_ORG_ID",
  "partner": "ORACLE",
  "service": "MARKETPLACE",
  "offerType": "PRIVATE",
  "productID": "KVvfb0Vhv",
  "buyerID": "gHYis9RKU",
  "name": "Acme Corp - Annual Contract 2026",
  "status": "DRAFT",
  "info": {
    "currency": "USD",
    "visibility": "PRIVATE",
    "deliveryMethod": "PRIVATE"
  }
}
```

Keep the returned `id`. It is the Suger offer ID used for every follow-up call, and it doubles as the
idempotency token for the OCI create — a retried submission will not produce a duplicate
buyer-visible offer.

## Track the offer

```shell
curl -L -X GET 'https://api.suger.cloud/org/YOUR_ORG_ID/offer/FIwCbefqu' \
  -H 'Authorization: Bearer YOUR_ACCESS_TOKEN'
```

Once OCI has the offer, `info.oracleOffer.offerID` holds the private-offer OCID and
`info.oracleOffer.offerStatus` holds Oracle's raw status. Suger maps that raw status onto its own:

| Oracle `offerStatus` | Suger `status` |
|---|---|
| `DRAFT` | `DRAFT` |
| `PENDING_MARKETPLACE` | `PENDING_MARKETPLACE_APPROVAL` |
| `PENDING_BUYER` | `PENDING_ACCEPTANCE` |
| `ACCEPTED` | `ACCEPTED` |
| `ACTIVE` | `ACTIVE` |
| `ENDED`, `EXPIRED` | `EXPIRED` |
| `FAILED_SEND`, `FAILED_ACCEPT` | `CREATE_FAILED` |

When the offer reaches `ACTIVE`, Suger derives an **entitlement** for the buyer. Buyers are deduped by
tenancy OCID, so repeat deals with the same customer attach to the same buyer record.

## Withdraw an offer

```shell
curl -L -X POST 'https://api.suger.cloud/org/YOUR_ORG_ID/offer/FIwCbefqu/cancel' \
  -H 'Authorization: Bearer YOUR_ACCESS_TOKEN'
```

Oracle allows withdrawing any offer the buyer has not yet accepted, so this is valid while the offer is
`PENDING_ACCEPTANCE` or `PENDING_MARKETPLACE_APPROVAL`. Once accepted, it can no longer be withdrawn.

## Validation errors

These are returned as `400` before anything is created in OCI, so a rejected request never leaves an
orphaned draft in your marketplace account.

| Message | Fix |
|---|---|
| `invalid offer type ... to create private offer in Oracle Marketplace` | Set `offerType` to `PRIVATE`. |
| `offer name is required` | Set `name`. |
| `info.eulaUrl is required` | Provide a contract document. |
| `info.eulaUrl ... must be an https URL` | Use `https`, not `http`. |
| `info.oracleOffer.sellerPrimaryContact (with email) is required` | Add the seller contact with an email. |
| `info.oracleOffer.buyerPrimaryContact (with email) is required` | Add the buyer contact with an email. |
| `buyer tenancy OCID is required ...` | Set `info.oracleOffer.buyerTenancyOCID`, or link an Oracle buyer via `buyerID`. |

Two further checks need OCI context and therefore surface after the API has returned, as a
`CREATE_FAILED` offer rather than a `400`:

- The product must resolve to an Oracle listing OCID.
- A linked `buyerID` must belong to partner `ORACLE`; a buyer imported from another marketplace is
  refused.
