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:
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
contractVersionis 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.
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 ashandoffToken - [ ] 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
externalReferenceset to your own tenant id - [ ] Confirm retried on timeout rather than treated as failed
- [ ] Branching on
fulfillmentState, never oncontractStatus - [ ] 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
