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 are told depends on whether you registered a receiver on the channel. See Where buyers go, and how you are told.
Without a 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 receiver — the signed webhook¶
Omnistrate posts contract.discovered to your receiver. 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"
}
}
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 five 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();
}
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.
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 the handoff |
Event types¶
Five, 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 | Re-read plan and adjust their limits |
contract.suspended | Suspended upstream, usually non-payment | Restrict access. Do not delete data |
contract.cancelled | Cancelled or expired | Begin your own offboarding |
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 - [ ] Whole flow rehearsed end to end in the sandbox
