Skip to content

API Guide

Everything you have to build, and the exact shapes involved. All payloads on this page are real responses from a live environment, not illustrations.

Base URL: https://api.omnistrate.cloud/2022-09-01-00/fleet/marketplace

All API calls authenticate with your own Omnistrate credential: Authorization: Bearer $TOKEN.

The One Call You Must Make

Everything else on this page is optional or situational. Confirm is not. Until you call it, no subscription exists, the buyer cannot deploy, and the marketplace is charging them anyway.

Receive the buyer

Step 1. How you receive the initial handoff depends on whether contract.discovered has a receiver in the channel's Event routing tab. See Configure the buyer handoff and event routing.

Without a contract.discovered receiver — the browser redirect

Omnistrate redirects the buyer to your callback URL with a credential appended:

GET https://portal.acme.example.com/marketplace/callback?code=hoff_YSYSQA5G489PMVSGGMPV5XB545

The query parameter is named code, and its value is a handoff token — you send it to the redeem endpoint as handoffToken. Nothing about the contract travels in the query string, because anything in a query string can be forged.

With a contract.discovered receiver — the signed webhook

Omnistrate posts contract.discovered to the receiver configured for that event. It is self-contained: everything needed to provision and confirm is in the body, so no follow-up call is required. It is also the only event that carries handoffToken.

POST https://hooks.acme.example.com/omnistrate
Content-Type: application/json
X-Omnistrate-Event-Id: evt-3c3d521bd975
X-Omnistrate-Event-Type: contract.discovered
X-Omnistrate-Timestamp: 2026-08-25T07:58:45Z
X-Omnistrate-Delivery-Attempt: 1
X-Omnistrate-Signature-256: sha256=24be8a3ec41a191588bb4a28cf5cef833f8fcef9050eada0fbd2d2b140b292b8
{
  "eventId": "evt-3c3d521bd975",
  "eventType": "contract.discovered",
  "occurredAt": "2026-08-25T07:58:45Z",
  "contractVersion": 1,
  "marketplaceContractId": "mkc-i2KCYuleCx",
  "channel": "SANDBOX",
  "externalRef": "sbx-ent-0304eca29fefbc75",
  "buyerRef": "sbx-buyer-7b92e1dd6313",
  "contractStatus": "PENDING",
  "fulfillmentState": "AWAITING_ISV",
  "detectedBy": "EVENT",
  "handoffToken": "hoff_CSW8X7RTYS35CVNZDRT6W4M0TR",
  "handoffExpiresAt": "2026-09-01T07:58:41Z",
  "org": {
    "orgId": "org-zt5jppbqe0",
    "rootUserId": "user-YevTFtlupB",
    "syntheticEmail": "[email protected]"
  },
  "subscriptionRequest": {
    "id": "subr-ljC4UXuw3M",
    "serviceId": "s-ACXVeEr9SN",
    "environmentId": "se-hKwSmAzDp0",
    "productTierId": "pt-PpEw0Ai7JA",
    "status": "PENDING"
  },
  "plan": {
    "planRef": "sbx-plan-standard-hl7r",
    "quantity": 40,
    "currency": "USD",
    "startsAt": "2026-08-25T07:58:41Z"
  },
  "capabilities": {
    "canHoldContract": true,
    "canReportProgress": true,
    "canCancel": true,
    "usageGate": "SOFT"
  }
}

For Suger-backed contracts, webhooks also include the optional top-level fields externalBuyerId and externalProductId when Suger supplies them. These are the marketplace-native identifiers used for billing; buyerRef and externalRef remain Suger's buyer and entitlement identifiers.

Verifying a delivery

Five headers accompany every request:

Header Value
X-Omnistrate-Event-Id Stable across retries. Deduplicate on it
X-Omnistrate-Event-Type One of the seven event types
X-Omnistrate-Timestamp RFC 3339. Reject anything more than five minutes old
X-Omnistrate-Delivery-Attempt 1 on the first try, incrementing
X-Omnistrate-Signature-256 sha256=<hex>, HMAC-SHA256

Sign the timestamp with the body

The signed material is <X-Omnistrate-Timestamp> + . + <raw request body> — not the body alone. Binding the timestamp in is what makes the five minute window enforceable: without it, a captured delivery could be replayed forever with a fresh timestamp.

Verify against the raw bytes, before any parsing or re-serialization. A re-encoded body will not match.

const timestamp = req.headers['x-omnistrate-timestamp'];
const signature = req.headers['x-omnistrate-signature-256'];
if (typeof timestamp !== 'string' || typeof signature !== 'string') {
  return res.status(401).end();
}

const signed = `${timestamp}.${rawBody}`;
const expected = 'sha256=' + crypto
  .createHmac('sha256', process.env.OMNISTRATE_WEBHOOK_SECRET)
  .update(signed, 'utf8')
  .digest('hex');

const expectedBuf = Buffer.from(expected, 'utf8');
const signatureBuf = Buffer.from(signature, 'utf8');
if (expectedBuf.length !== signatureBuf.length || !crypto.timingSafeEqual(expectedBuf, signatureBuf)) {
  return res.status(401).end();
}

Event receiver requirements

  • HTTPS only. Private, loopback, link-local and cloud metadata addresses are refused, at registration and again at delivery time.
  • Answer within 10 seconds with any 2xx. Do the work asynchronously.
  • Deduplicate on eventId. The same event will arrive more than once. That is normal, not an incident.
  • Discard out-of-order deliveries by dropping any event whose contractVersion is strictly lower than the highest you have applied for that contract. Process an equal version: a redelivery you asked for reuses the version deliberately.

Failed deliveries are retried with backoff for 24 hours, then abandoned and surfaced to you and to Omnistrate support.

Each of the seven event types has its own route. Routes may point to different endpoints, and an event with no configured route is not delivered. Only the contract.discovered route controls whether the initial credential is delivered by signed webhook or browser redirect.

Redeem the handoff

Step 2. Exchange the credential for the contract detail.

curl -X POST https://api.omnistrate.cloud/2022-09-01-00/fleet/marketplace/handoff/redeem \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"handoffToken":"hoff_YSYSQA5G489PMVSGGMPV5XB545"}'

The same field carries both arrival paths: the ?code= value from your callback URL, and the handoffToken from a contract.discovered body.

200 · Response

{
  "marketplaceContractId": "mkc-D1uAiFE37D",
  "channel": "SANDBOX",
  "externalRef": "sbx-ent-3045f52c54baa041",
  "buyerRef": "sbx-buyer-36d2a83bddd7",
  "contractStatus": "PENDING",
  "fulfillmentState": "IDENTIFIED",
  "contractVersion": 1,
  "org": {
    "orgId": "org-wg28gu8yc1",
    "rootUserId": "user-76x7Jo9AYT",
    "syntheticEmail": "[email protected]"
  },
  "plan": {
    "planRef": "sbx-plan-standard-hl7r",
    "quantity": 25,
    "currency": "USD",
    "startsAt": "2026-08-25T20:42:28Z"
  },
  "capabilities": {
    "canHoldContract": true,
    "canReportProgress": true,
    "canCancel": true,
    "usageGate": "SOFT"
  },
  "handoffSlaExpiresAt": "2026-08-26T20:42:28Z",
  "handoffTokenExpiresAt": "2026-09-01T20:42:28Z"
}

Behaviour

  • Idempotent and non-consuming. The same token returns the same body until it expires, so a failed provisioning attempt can simply be retried. Making it one-shot would buy nothing against a leaked URL and would turn a buyer refreshing your page into an error.
  • 410 once expired. 404 for a token that is unknown or belongs to another organization — the two are deliberately indistinguishable.

fulfillmentState may read IDENTIFIED, not AWAITING_ISV

Redeem immediately after a redirect and the run may not have reached the handoff stage yet. The contract is still yours to confirm; the state is a snapshot of a workflow that is still moving.

Confirm fulfillment

Step 3, and the one that matters. Call it once the buyer's tenant exists in your product and you are ready to serve them. It moves the subscription request from PENDING to APPROVED, and that approval is what creates the subscription.

curl -X POST https://api.omnistrate.cloud/2022-09-01-00\
/fleet/marketplace/contract/mkc-D1uAiFE37D/fulfillment/confirm \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"externalReference":"acme-tenant-4821"}'
Field Required Meaning
id (path) Yes marketplaceContractId from the event or redeem body
externalReference No Your own id for the tenant you provisioned. Recorded against the contract, so a support conversation can start from your identifier rather than ours

200 · Response

{
  "marketplaceContractId": "mkc-D1uAiFE37D",
  "fulfillmentState": "READY",
  "currentStage": "READY",
  "workflowId": "marketplace-fulfillment-mkc-D1uAiFE37D-v1",
  "subscriptionId": "sub-qNFbtgJFaV",
  "blockedOn": "deployments are allowed, and the channel has not yet opened the usage gate, so nothing is being metered",
  "stages": [ /* every stage, with its status and timings */ ]
}

The response is the confirmation

The call is synchronous. It waits for the approval and returns the subscriptionId it created. There is no second call and no webhook to wait for.

It is idempotent. Confirming twice creates no second subscription: the second call returns the subscription the first produced. Retrying after a timeout is always safe, and it is the intended recovery if you never saw the response.

400 if fulfillment is not waiting for a decision, for example a contract already closed. 404 if the contract is unknown or belongs to another organization.

Where to confirm from the console

An operator can confirm the same contract by hand from FinOps Center → Marketplace Contracts. The action sits on the contract itself, beside Deny, Retry and Cancel.

Confirming a contract from the console

This is the same code path the API uses. It exists for the case where your integration is down and a buyer is waiting, not as the normal route.

Deny fulfillment

Refuse the buyer. No subscription is created and the request is marked denied. Use it when you cannot serve this customer — a conflicting agreement, a sanctions check, an unsupported region.

curl -X POST https://api.omnistrate.cloud/2022-09-01-00\
/fleet/marketplace/contract/mkc-D1uAiFE37D/fulfillment/deny \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"reason":"Buyer domain is covered by an existing agreement"}'

reason is optional and you should send it. It is recorded and shown to operators on both sides, because a refused marketplace purchase is a conversation somebody will have to have.

Reading a contract

Call Returns
GET /contract Every contract for your organization. Add ?includeSimulated=true to include sandbox rehearsals
GET /contract/{id} One contract, as the channel reports it
GET /contract/{id}/fulfillment The fulfillment run: stages, timings, blockedOn, and the workflow id
POST /contract/{id}/fulfillment/retry Re-enter the run and re-emit contract.discovered when that event has a receiver

For callback-only integrations, retry does not reopen the buyer's browser or repeat the redirect. Retry the redeem and confirm flow with the token from the original callback.

Event Types

Seven, and the set is fixed. Dispatch on eventType.

Type Meaning You should
contract.discovered A buyer purchased and their organization and pending request exist Provision your tenant, then confirm
entitlement.updated The buyer changed seats, Plan, or term; Omnistrate does not automatically move the subscription to another Plan Re-read plan and adjust their limits or product configuration
contract.suspended Suspended upstream, usually non-payment Restrict access. Do not delete data
contract.reinstated An upstream suspension was lifted and Omnistrate restored subscription access Restore access in your product
contract.ending A scheduled cancellation is approaching while the contract remains active Keep serving the buyer and submit remaining usage within the reported window
contract.cancelled Cancelled or expired; Omnistrate has revoked subscription access Begin your own offboarding and retention process
fulfillment.failed Fulfillment could not proceed, most often because confirm was never called in time Read failureReason. If you can now proceed, confirm

Be idempotent on buyerRef

Not because you will routinely get two arrivals, but because you can: a redelivery you asked for, a retry after your endpoint timed out, or a buyer returning to a bookmarked callback link all produce a second arrival for one buyer. Key the tenant on buyerRef and a repeat becomes a no-op instead of a second tenant.

Integration checklist

  • [ ] Callback URL registered, HTTPS, no query string of your own
  • [ ] Callback handler reads ?code= and calls redeem with it as handoffToken
  • [ ] Receiver registered if you need lifecycle events after the purchase
  • [ ] Signature verified over timestamp + "." + raw body, in constant time
  • [ ] Replay window enforced at five minutes
  • [ ] Deduplicating on eventId
  • [ ] Dropping events with a lower contractVersion
  • [ ] Provisioning keyed on buyerRef, so a repeat is a no-op
  • [ ] Confirm called, with externalReference set to your own tenant id
  • [ ] Confirm retried on timeout rather than treated as failed
  • [ ] Branching on fulfillmentState, never on contractStatus
  • [ ] Lifecycle handlers implemented for updates, suspension, reinstatement, ending and cancellation
  • [ ] Whole flow rehearsed end to end in the sandbox
  • [ ] Marketplace usage verified as described in Billing and Usage Reporting