Skip to content

Creating Contracts ​

Learn how to create invoiced contracts, including multi-seat contracts, through the Management API without sending the customer through a checkout.

Overview ​

A contract created through the Management API is the same thing as one created with Create contract in the Sesamy portal. You pick a product and a purchase option, name who gets access, and Sesamy invoices the customer. It suits B2B sales, where a deal is agreed outside the checkout and often at a negotiated price.

Creating a contract takes two steps:

  1. Create the contract. It comes back as PENDING: priced, but not billed and granting no access.
  2. Confirm it. Sesamy sends the invoice and grants access.

The gap between the two lets you check the price and the discounts before an invoice reaches your customer. You can do both in one request with confirm: true.

Creating a contract is not idempotent

A repeated POST /management/contracts creates a second contract. If a request times out, look the contract up with GET /management/contracts?userId=... before you retry. Confirming is safe to repeat: confirming an already confirmed contract returns it unchanged.

Base URL ​

https://api2.sesamy.com

Authentication ​

Both endpoints need a Management API token with the contracts:write scope. Request the token from https://token.sesamy.com/oauth/token with the client credentials flow, and send it as a bearer token:

Authorization: Bearer YOUR_ACCESS_TOKEN

See Authentication for how to obtain a token.

Getting access

The contracts:write scope is not part of the API key presets. Contact Sesamy to have it enabled for your client.

Create a Contract ​

POST https://api2.sesamy.com/management/contracts

Creates a PENDING contract. The product must belong to your vendor.

FieldTypeDescription
skustringThe product to sell
purchaseOptionIdstringThe purchase option of that product
contractTypestring(Optional) PERSONAL or BUSINESS. Defaults to PERSONAL
userobjectWho gets access. Sesamy creates a user for the email if none exists
user.emailstringThe user's email address
user.firstNamestring(Optional)
user.lastNamestring(Optional)
user.phonestring(Optional)
user.businessDetailsobject(Optional) companyName and vatNr
user.addressobject(Optional) Billing address: street, zip, city, and a two-letter country, plus optional firstName, lastName, and co
user.deliveryAddressobject(Optional) Same shape as address
payerobject(Optional) Who is billed, when that is someone other than user. Same shape as user
invoice.emailstring(Optional) Where invoices are sent. Defaults to the email of whoever is billed
invoice.customerReferencestring(Optional) Your customer's reference, printed on the invoice as their reference. For example a purchase order number
invoice.vendorReferencestring(Optional) Your own reference, printed on the invoice as your reference. For example the seller. At most 34 characters
invoice.ediNumberstring(Optional) Delivers the invoice as an e-invoice (EDI/EHF) to this number
quantityinteger(Optional) Billed units. Defaults to 1. Must be within the limits of the purchase option
seatsinteger(Optional) Total seats. License products only. See Multi-Seat Contracts
unitPrice.amountnumber(Optional) Overrides the list price per unit, in the currency of the purchase option. Applies to every bill, not only the first
unitPrice.taxIncludedboolean(Optional) Whether unitPrice.amount includes VAT. Defaults to the setting of the purchase option
discountCodesstring[](Optional) Discount codes to apply
payment.providerstringBILLOGRAM
payment.methodstringINVOICE
payment.countrystring(Optional) Two-letter country the payment is made from. Decides the market and the VAT. Defaults to SE
startsAtstring(Optional) ISO 8601 date to start the contract on. Until then the contract is FORTHCOMING, and access is granted when it starts
anchorDatestring(Optional) ISO 8601 date to align the billing cycle to
confirmboolean(Optional) Confirm in the same request. Defaults to false

Prices are in major units, as everywhere else in the API: send 9999 for 9,999.00 SEK.

Why only invoices

A contract created from your server has nobody present to approve a card or a wallet payment, so it could be created but never confirmed. Send the customer through a checkout for those payment methods.

Example Request ​

bash
curl -X POST "https://api2.sesamy.com/management/contracts" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "sku": "sid:acme-plus-business",
    "purchaseOptionId": "acme-plus-business-yearly",
    "contractType": "BUSINESS",
    "user": {
      "email": "jane.doe@acme.example",
      "firstName": "Jane",
      "lastName": "Doe",
      "businessDetails": { "companyName": "ACME AB", "vatNr": "SE556000000001" },
      "address": {
        "street": "Example Street 1",
        "zip": "12345",
        "city": "Stockholm",
        "country": "SE"
      }
    },
    "invoice": {
      "email": "invoices@acme.example",
      "customerReference": "PO-1042",
      "vendorReference": "John Doe"
    },
    "quantity": 1,
    "seats": 10,
    "unitPrice": { "amount": 9999, "taxIncluded": true },
    "payment": { "provider": "BILLOGRAM", "method": "INVOICE", "country": "SE" }
  }'
typescript
const contract = await mgmtSdk.contracts.create({
  sku: 'sid:acme-plus-business',
  purchaseOptionId: 'acme-plus-business-yearly',
  contractType: 'BUSINESS',
  user: {
    email: 'jane.doe@acme.example',
    firstName: 'Jane',
    lastName: 'Doe',
    businessDetails: { companyName: 'ACME AB', vatNr: 'SE556000000001' },
  },
  invoice: { email: 'invoices@acme.example', customerReference: 'PO-1042' },
  quantity: 1,
  seats: 10,
  unitPrice: { amount: 9999, taxIncluded: true },
  payment: { provider: 'BILLOGRAM', method: 'INVOICE', country: 'SE' },
});

Example Response ​

Status: 201 Created

json
{
  "id": "M-V1StGXR8_Z5jdHi6B-myT",
  "name": "Contract for jane.doe@acme.example from portal",
  "userId": "auth2|user_123",
  "status": "PENDING",
  "isActive": false,
  "price": 9999,
  "currency": "SEK",
  "contractDuration": "RECURRING",
  "recurringInterval": "YEAR",
  "recurringTime": 1,
  "origin": { "type": "MANUAL", "id": "V1StGXR8_Z5jdHi6B-myT" },
  "items": [
    {
      "sku": "sid:acme-plus-business",
      "name": "ACME Plus Business",
      "purchaseOptionId": "acme-plus-business-yearly",
      "productType": "license",
      "isMultiseat": true,
      "quantity": 1,
      "seats": 10
    }
  ],
  "appliedDiscounts": [],
  "discount": null
}

Check the Discounts Before You Confirm ​

A discount code that does not apply never fails the request. The contract is created at full price, and the code is listed with "status": "UNAPPLICABLE" in appliedDiscounts. Check it before you confirm.

With confirm: true there is no moment to check, so the API does it for you: if a code could not be applied, the request fails with 400, and the contract is left PENDING and unbilled.

Confirm a Contract ​

POST https://api2.sesamy.com/management/contracts/{id}/confirm

Confirms a PENDING contract. Sesamy sends the invoice and grants access, including the seats of a multi-seat contract. The request has no body.

bash
curl -X POST "https://api2.sesamy.com/management/contracts/M-V1StGXR8_Z5jdHi6B-myT/confirm" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
typescript
const confirmed = await mgmtSdk.contracts.confirm('M-V1StGXR8_Z5jdHi6B-myT');

The response is the contract, now ACTIVE. A contract with a startsAt in the future becomes FORTHCOMING instead, and turns ACTIVE on that date.

StatusMeaning
200The contract is confirmed. Also returned for a contract that was already confirmed
400The payment provider refused. The message says why
404No such contract for your vendor
409The contract is in a status that cannot be confirmed, such as CANCELED

Multi-Seat Contracts ​

A multi-seat contract gives several people access under one contract. To create one, sell a product of the License type. Two fields decide its size:

  • quantity is what you bill. The price is quantity × the unit price.
  • seats is how many people get access. Leave it out and the contract gets the seats per unit of the purchase option × quantity. Set it to grant a different number of seats without changing the price.

A flat price for a fixed number of seats is quantity: 1 with unitPrice and seats set, as in the example above: one unit, 9,999.00 SEK, ten seats. To bill per seat instead, send quantity: 10 with the price of one seat.

The user administers the seats and holds the first one. To see everyone assigned a seat, call GET /management/contracts/{id}/seats/users.

Setting seats on a product that is not a license fails with 400.

Change the Seats ​

PUT https://api2.sesamy.com/management/contracts/{id}/seats

Sets the total number of seats on a contract that is ACTIVE, FORTHCOMING or OVERDUE. Billing stays the same: quantity and the price are untouched, and the change applies at once. To bill for the extra seats, change the price separately.

bash
curl -X PUT "https://api2.sesamy.com/management/contracts/M-V1StGXR8_Z5jdHi6B-myT/seats" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "seats": 25 }'
typescript
const contract = await mgmtSdk.contracts.setSeats('M-V1StGXR8_Z5jdHi6B-myT', { seats: 25 });
FieldTypeDescription
seatsintegerThe total number of seats, at least 1
skustring(Optional) The license item to change. Only needed when the contract has more than one

The response is the contract with the new seats on the item. Setting the number of seats the contract already has returns it unchanged, so the request is safe to repeat. People get their seats shortly after the change, not in the same request.

You cannot lower the seats below the number of people holding one. List and remove seat users first.

StatusMeaning
200The seats are set
400The body is invalid, the contract has no license item, sku does not match an item, or sku is missing on a contract with more than one license item
404No such contract for your vendor
409The contract is not active or is set to cancel, more people hold a seat than seats, or Sesamy could not confirm that the price stays the same

Manage Seat Users ​

Use these routes to manage who holds the seats on a multi-seat contract:

MethodRoutePurpose
GET/management/contracts/{id}/seats/usersList seat users. Pass ?sku=... to filter by license item; omit it to list all items.
POST/management/contracts/{id}/seats/usersAssign a seat by email. Returns 204.
DELETE/management/contracts/{id}/seats/users/{userId}Remove a seat user by ID. Returns 204.

The list returns each user's userId, sku, status, and entitlementId, plus email and fullName when available. Removed seats are left out. The sku is the content product the seat unlocks, so it can differ from the license product SKU on the contract item. For example, a seat on sid:acme-plus-business-multi is listed as sid:acme-plus-business. The sku filter accepts either form. Use the returned userId for removal. These routes require contracts:read for listing and contracts:write for assigning or removing.

bash
curl "https://api2.sesamy.com/management/contracts/M-V1StGXR8_Z5jdHi6B-myT/seats/users" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

curl -X POST "https://api2.sesamy.com/management/contracts/M-V1StGXR8_Z5jdHi6B-myT/seats/users" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "email": "reader@example.com" }'

curl -X DELETE "https://api2.sesamy.com/management/contracts/M-V1StGXR8_Z5jdHi6B-myT/seats/users/auth0%7Creader" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
typescript
await mgmtSdk.contracts.addSeatUser(contractId, { email: 'reader@example.com' });
const users = await mgmtSdk.contracts.listSeatUsers(contractId);
const reader = users.find((user) => user.email === 'reader@example.com');
if (reader) await mgmtSdk.contracts.removeSeatUser(contractId, reader.userId);

When every seat is taken, POST returns 409. GET /management/contracts/{id}/licenses remains available for existing integrations but is deprecated.

User and Payer ​

user is who gets access. payer is who gets the bill. Leave payer out when they are the same, which is the common case: put the company details and the billing address on user.

Set payer when the invoice goes to someone else, such as a finance contact or a parent company. Invoices are then addressed to the payer on behalf of the user, and invoice.email applies to the payer. The contract names the payer in its payer field:

json
{
  "userId": "auth2|user_123",
  "payer": { "id": "auth2|user_456", "email": "finance@acme.example" }
}

Errors ​

StatusWhen
400The body is invalid, the purchase option does not exist, quantity is outside its limits, seats is set on a product that is not a license, the payment method is not available to you, or a discount code could not be applied with confirm: true
403The token lacks the contracts:write scope
404The product does not exist, or belongs to another vendor
503Sesamy could not reach the payment service. The contract may or may not exist: look it up before you retry

See Errors for the shape of an error response.

Next Steps ​

Released under the MIT License.