DigitalOcean Marketplace Vendor SaaS Add-Ons API Documentation

Getting Started

DigitalOcean Marketplace engineers will support your team as they onboard your Software-as-a Service Add-On to the DigitalOcean Marketplace. To get started, you need a DigitalOcean account to create a Team to later associate with your Add-On, and then you’ll need to register as a Vendor.

Once approved, you’ll be able to access the Marketplace Vendor Portal, where you’ll start by creating your Add-On listing. Your listing is home to your SaaS Add-On’s configuration information, as well as public-facing information for prospective users to use to evaluate your services.

Definitions

Before getting started, familiarize yourself with the terminology used for DigitalOcean Marketplace Vendors. You’ll see these throughout the interface and documentation.

Add-On = Software-as-a-service made available through the DigitalOcean Marketplace.

App = The vendor’s application that the DigitalOcean API talks to.

Resource = An instance of an Add-On the user provisions through the DigitalOcean Marketplace. A Marketplace resource typically maps one-to-one with an account on your SaaS service.

Vendor = The person or company whose Add-On is available on the DigitalOcean Marketplace.

Creating Your Listing

When creating your Add-On listing, you’ll want to have both SEO and naming best practices in mind. For instance, SaaS Company, Inc. would be the name of the Vendor, and the Add-On’s app_slug (app API identifier) would be saas-name. Most vendors have multiple Add-On listings, including one for each of their production and staging environments. To continue our example, the Add-On for the staging environment may have an app_slug of saas-name-staging. For each Add-On, you'll list your payment plans and the features that come with each plan, as well as provide information about support and additional resources.

When you create an Add-On listing, you'll be given a set of credentials including a password, salt, and client_secret. Store these securely on your end. You will use the password to authenticate API requests coming from DigitalOcean to your app, the salt to authenticate SSO requests, and the client_secret to exchange an authorization_code for an access_token and refresh_token, to be used when making requests back to DigitalOcean's SaaS API.

Authentication

When DigitalOcean makes API requests to your app's endpoints, we will utilize basic auth. The auth header will contain a base64 encoded string consisting of your app's slug and the pre-shared password in the format: slug:password. For example, an app whose slug is acme and password is 1234 will receive requests with the following authorization header: Authorization: Basic YWNtZToxMjM0.

Retries

A request may be sent multiple times. Therefore, if a request includes a uuid, the response needs to be idempotent.

Timeouts

Your app should respond within no more than 30 seconds. If you need more than 30 seconds to handle the provision request, please use the asynchronous provisioning option.

Provision

When a DigitalOcean user installs an Add-On, DigitalOcean will send a resource provisioning request to your app’s registered endpoint:

// POST /digitalocean/resources
// Content-Type: application/json
// Authorization: Basic YWNtZToxMjM0

{
  // User selected app slug as provided by the vendor during vendor registration
  "app_slug": "example",

  // User selected plan slug as provided by the vendor during vendor registration
  "plan_slug": "premium",

  // DigitalOcean generated UUID for identifying this specific resource
  "uuid": "RESOURCE_UUID",

  // A name for the resource that the user provided during the provisioning flow.
  "resource_name": "RESOURCE_NAME",

  // The callback URL your app will make a request to if you use the asynchronous provisioning option.
  "callback_url": "CALLBACK_URL",

  // Customizable metadata that a DigitalOcean user can set for this specific resource
  "metadata": { "region": "nyc", "foo": "bar" },

  // An obfuscated email pointing to the user’s email address. Anything sent to this email will be
  // forwarded to the user.
  "email": "<uuid>@digitalocean.com",

  // DigitalOcean obfuscated ID that will uniquely identify the user's team. This is useful to know
  // when the same DigitalOcean team provisions multiple resources for your Add-On.
  "creator_id": "CREATOR_ID",

  // Your app can asynchronously exchange this authorization_code for an access_token and
  // refresh_token upon success. With the access_token, you can modify this specific resource within
  // DigitalOcean.
  "oauth_grant": { 
    "type": "authorization_code", 
    "code": "AUTHORIZATION_CODE_UUID",
    "expires_at": 1620915831
  }
}

Your app will then respond with the following:

// HTTP 200 or 201

{
  // Required: An immutable value for DigitalOcean to reference this resource within your app.
  // Can be the resource’s UUID from the originating request.
  "id": "RESOURCE_ID", 

  // The variables necessary to enable the DigitalOcean user to utilize your app
  // (endpoints, credentials, etc). They will be displayed to the user from the Add-On pages
  // on DigitalOcean. The variables will be prefixed with your Add-On’s Configuration Variable
  // Prefix config_vars_prefix. It is recommended that your variables follow the ALL_CAPS naming 
  // convention.
  "config": {
    "EXAMPLE_ENDPOINT": "https://do-user:password@api.example.com/v1",
    "EXAMPLE_FOO": "BAR"
  },

  // Optional
  "message": "A message that will be displayed to the DigitalOcean user upon completion of the request."
}

Your app can use the config section of the response to send back information customers will need to use your Add-On. This is typically API keys, login details, or other private credentials, but could be used to provide other types of information as well.

The customer will be able to access this through the Add-On UI in the Control Panel. When displayed, the “configuration variable prefix” will be appended to the variable name you supply.

Config variables can also be updated as described below.

In the event that the resource cannot be created, respond with the following:

// HTTP 422 

{ 
  // Optional
  "message": "A message that will be displayed to the DigitalOcean user as to why the resource could not be created."
}

If the provision request was successful, but your app is using the asynchronous provisioning option, respond with the following:

// HTTP 202

Asynchronous Provisioning

If your app needs more than 30 seconds to handle the resource provision request, you may use the asynchronous provisioning option.

The first step is to have your app respond to the resource provision request with the following:

// HTTP 202

Then, follow the instructions for the Authorization Code Exchange. You will need an access_token to asynchronously provision this resource.

Finally, once your app has the access_token for the resource and has completed provisioning, it will make a POST request to the callback_url value provided in the original provision request. Make sure to include the access_token in the Authorization header:

// Content-Type: application/json
// Authorization: Bearer YWNtZToxMjM0

{
  // Required: An immutable value for DigitalOcean to reference this resource within your app.
  // Can be the resource’s UUID from the originating request.
  "id": "RESOURCE_ID", 

  // Required: either one of two values — ”PROVISIONED” if the resource provisioning was successful,
  // or “FAILED” if the provisioning failed.
  "state": "PROVISIONED",

  // The variables necessary to enable the DigitalOcean user to utilize your app
  // (endpoints, credentials, etc). They will be displayed to the user from the Add-On pages
  // on DigitalOcean. The variables will be prefixed with your Add-On’s Configuration Variable
  // Prefix config_vars_prefix. It is recommended that your variables follow the ALL_CAPS naming 
  // convention.
  "config": { 
    "EXAMPLE_ENDPOINT": "https://do-user:password@api.example.com/v1",
    "EXAMPLE_FOO": "BAR"
  },

  // Optional
  "message": "A message that will be displayed to the DigitalOcean user upon completion of the request."
}

Deprovision

When a DigitalOcean user requests that a resource be destroyed, DigitalOcean will send the following request to your app’s registered endpoint:

// DELETE /digitalocean/resources/:resource_uuid
// Content-Type: application/json
// Authorization: Basic YWNtZToxMjM0

{}

Your app will then respond with the following:

// HTTP 200 or 204

In the event that the resource cannot be found, respond with the following:

// HTTP 410

In the event that the resource cannot be destroyed, respond with the following:

// HTTP 422

{
  // Optional
  "message": "A message that will be displayed to the DigitalOcean user as to why the resource could not be destroyed."
}

Plan Change (Upgrade/Downgrade)

DigitalOcean users can request to change their plan at any time through the DigitalOcean control panel. Plans cannot be changed by Vendors or within the Vendor application. When a DigitalOcean user requests a plan change, DigitalOcean will send the following request to your app’s registered endpoint:

// PUT /digitalocean/resources/:resource_uuid
// Content-Type: application/json
// Authorization: Basic YWNtZToxMjM0

{
  "plan_slug": "newly-selected-plan"
}

Your app will then respond with the following:

// HTTP 200

{
  // Optional
  "message": "A message that will be displayed to the DigitalOcean user upon completion of the request."
}

If you do not wish to return a message, respond with the following instead:

// HTTP 204

In the event that the plan cannot be changed, respond with the following:

// HTTP 422

{
  // Optional
  "message": "A message that will be displayed to the DigitalOcean user as to why the plan couldn't be changed."
}

In the event that the resource cannot be found, respond with the following:

// HTTP 404

Notifications

Occasionally, DigitalOcean will send important notifications regarding your app or its users. These are often sent by email and by POSTing to a notifications endpoint on your app. The types of notifications we send are summarized here, but could be extended in the future. 

Resource Suspension

resources.suspended 

A resource may be suspended if a user is late paying their invoice. DigitalOcean will notify your app when one or more resources have been suspended with a request of type resources.suspended.

// POST /digitalocean/notifications
// Content-Type: application/json
// Authorization: Basic YWNtZToxMjM0

{
  "type": "resources.suspended", 
  "created_at": 1620915831,
  "payload": {
    "resources_uuids": []
  }
}

Resource Reactivation

resources.reactivated

A resource may be reactivated if a user pays their late invoice. DigitalOcean will notify your app when one or more resources have been reactivated with a request of type resources.reactivated.

// POST /digitalocean/notifications
// Content-Type: application/json
// Authorization: Basic YWNtZToxMjM0

{
  "type": "resources.reactivated", 
  "created_at": 1620915831,
  "payload": {
    "resources_uuids": []
  }
}

Resource Deprovisioning Failure

resources.deprovisioning.failed

If a user destroys a resource, your app will be sent a deprovisioning request as described above. If your app does not correctly handle the request, we will notify you with a notification of type resources.deprovisioning.failed.

// POST /digitalocean/notifications
// Content-Type: application/json
// Authorization: Basic YWNtZToxMjM0

{
  "type": "resources.deprovisioning.failed", 
  "created_at": 1620915831,
  "payload": {
    "resources_uuids": []
  }
}

Resource Updated

resources.updated

A user has the option to rename a resource. Upon renaming one of their Add-On resources, DigitalOcean will send you a resources.updated request, which includes the full state of the resource.

// POST /digitalocean/notifications
// Content-Type: application/json
// Authorization: Basic YWNtZToxMjM0

{
  "type": "resources.updated", 
  "created_at": 1620915831,
  "payload": {
    "resource": {
      "uuid": "UUID",
      "name": "NAME",
      "state": "STATE",
      "created_at": 1620915831,
      "updated_at": 1620915831
    },
    "plan": {
      "display_name": "NAME",
      "slug": "SLUG",
      "created_at": 1620915831,
      "updated_at" : 1620915831
    }
  }
}

Single Sign-On

DigitalOcean users of your SaaS Add-On should be able to log into your app’s dashboard. When a user wants to SSO from DigitalOcean to your app’s dashboard, we will send the following request to your app’s registered endpoint:

// POST /digitalocean/sso
// Content-Type: application/x-www-form-urlencoded

resource_uuid=RESOURCE_UUID&auth_token=abc123abc123abc123abc123abc123&timestamp=1620912812&user_email=<obfuscated>@marketplace.digitalocean.com&user_id=<obfuscated>

Parameters:

resource_uuid: The UUID provided by DigitalOcean during provisioning.

auth_token: Hex encoded, HMAC SHA256 encrypted hash of "timestamp:resource_uuid:user_id:user_email" with the salt as the HMAC key; this is used to authenticate the request

timestamp: Unix timestamp of when the SSO request was made

user_email: An obfuscated email that is linked to the user logging in to your app.

user_id: An obfuscated string representing the user’s uuid.

Your app will need to validate the request from DigitalOcean by verifying the auth_token. This is done by creating your own HMAC hash from timestamp:resource_uuid:user_id:user_email, using the salt as the secret key and SHA256 as the message digest algorithm, and then Hex encoding it. The resource_uuid, user_id, user_email, and timestamp will be in the request. The salt was provided to you when you created your Add-On listing.

Note: It is up to you to “expire” tokens using the timestamp provided.  A best practice would be to reject any tokens with a timestamp older than a minute or two.

Note: You may see a parameter in the request called token in addition to auth_token. This is a deprecated value included solely to support legacy integrations during a migration period and should not be used for your SSO integration. Use auth_token to validate your request.

Here are code examples for validating the token.

package main

import (
  "crypto/hmac"
  "crypto/sha256"
  "encoding/hex"
  "fmt"
)

const appSalt = "secretAppSalt"

func main() {
  checkCode := "6417516ede2053732f41480025f5adf7139c3ed925078db5ade2db9f1766f403"
  decodedCheckCode, err := hex.DecodeString(checkCode)
  if err != nil {
    panic(err)
  }
  timestamp := "timestamp1"
  resourceUUID := "resUUID"
  userID := "userID"
  userEmail := "userEmail"
  message := []byte(fmt.Sprintf("%s:%s:%s:%s", timestamp, resourceUUID, userID, userEmail))

  hash := hmac.New(sha256.New, []byte(appSalt))
  hash.Write(message)

  fmt.Println("check result: ", hmac.Equal(hash.Sum(nil), []byte(decodedCheckCode)))
}

import hmac
import hashlib
import base64

appSalt = b"secretAppSalt"
message = b"timestamp1:resUUID:userID:userEmail"
expected = "6417516ede2053732f41480025f5adf7139c3ed925078db5ade2db9f1766f403"

h = hmac.new(appSalt, message, hashlib.sha256)
hmac.compare_digest(expected, h.hexdigest())

require 'openssl'

salt = 'secretAppSalt'
data = 'timestamp1:resUUID:userID:userEmail'
digest = OpenSSL::Digest.new('sha256')

OpenSSL.secure_compare(OpenSSL::HMAC.hexdigest(digest, salt, data), "6417516ede2053732f41480025f5adf7139c3ed925078db5ade2db9f1766f403")

If the auth_token is valid, your app will create a session cookie for the user, and then respond with the following, allowing the user to proceed to your dashboard in a logged-in state:

// HTTP 302
// Location: https://www.example.com/dashboard

In the event that the auth_token cannot be verified, respond with the following:

// HTTP 401

Authorization Code Exchange

When a resource is provisioned, an authorization_code (which expires after 5 minutes) is provided that will need to be exchanged for an access_token and refresh_token. The access_token has a finite lifespan and gives you the ability to modify the variables for a specific resource. The refresh_token is used to retrieve a new access_token once the old one has expired.

Example authorization_code -

"oauth_grant": {
  "type": "authorization_code",
  "code": "AUTHORIZATION_CODE_UUID",
  "expires_at": 1620915831
}

To request a access_token and refresh_token:

// POST /v2/add-ons/oauth/token
// Host: api.digitalocean.com:443
// Content-Type: application/json

{
  // The authorization code provided during the provisioning request
  "code": "AUTHORIZATION_CODE_UUID",

  // Type of code
  "grant_type": "authorization_code",
 
  // The preshared secret that is associated with your Add-On
  "client_secret": "CLIENT_SECRET_UUID" 
}

// HTTP 200

{
  // Used to access the DigitalOcean API scoped to a single resource. Normally expires
  // every 8 hours, but may expire early in certain circumstances.
  "access_token": "ACCESS_TOKEN_UUID",

  // Valid for the lifetime of the resource and can be exchanged for a new access_token
  // as many times as needed using a valid OAuth client_secret.
  "refresh_token": "REFRESH_TOKEN_UUID",

  // The number of seconds the access_token is valid for. The refresh_token is used to
  // acquire a new access_token.
  "expires_in": 28800, 

  // The token type is used in the Authorization header of requests to the DigitalOcean API
  "token_type": "Bearer"
}

Access Token Refresh

If your apps access_token has expired, you will need to use the refresh_token returned above to request a new access_token, by making the following request:

// POST /v2/add-ons/oauth/token
// Host: api.digitalocean.com:443
// Content-Type: application/json

{
  // Type of code
  "grant_type": "refresh_token",

  // The authorization code provided during the provisioning request
  "refresh_token": "REFRESH_TOKEN_UUID", 

  // The preshared secret that is associated with your Add-On
  "client_secret": "CLIENT_SECRET_UUID"
}

DigitalOcean will respond with an updated access_token and a new refresh_token:

// HTTP 200

{
  // Used to access the DigitalOcean API scoped to a single resource. Normally expires
  // every 8 hours, but may expire early in certain circumstances.
  "access_token": "ACCESS_TOKEN_UUID",

  // Valid for the lifetime of the resource and can be exchanged for a new set of access_token
  // and refresh_token as many times as needed using a valid OAuth client_secret
  "refresh_token": "REFRESH_TOKEN_UUID", 

  // The number of seconds the access_token is valid for. The refresh_token is used to
  // acquire a new access_token.
  "expires_in": 28800,

  // The token type is used in the Authorization header of requests to the DigitalOcean API
  "token_type": "Bearer"
}

Resource Config Update

Your app can create or update config variables that a customer will need to use your Add-On. This is typically API keys, log in details, or other private credentials, but could be used to provide other types of information as well.

The customer will be able to access this through their Add-On UI. When displayed, the “configuration variable prefix” will be appended to the variable name you supply.

Using an access_token, your app can add to, or update the config variables for a given resource.

To update the config variables for a given resource send the access token in the Authorization header.

// PATCH /v2/add-ons/resources/:resource_uuid/config
// Host: api.digitalocean.com:443
// Content-Type: application/json
// Authorization: Bearer U1MJe2Gr2cyEyILEhaaol41uL3aDJERj

The body of your app's request should include the variables you wish to update alongside the value for each.

Note: variables can be updated, but cannot be destroyed. It is recommended that your variables follow the ALL_CAPS naming convention.

{
  "config": {
    "EXAMPLE_ENDPOINT": "https://do-user:password@api.example.com/v1",
    "EXAMPLE_FOO": "BAR"
  }
}

Metadata

When creating an Add-On listing, you have the option to add custom form fields that will be incorporated into the sign-up process. The values provided by a user will be stored with the resource as metadata. Metadata are sent to you during resource creation as part of the provisioning request.

Note: All custom fields you add are required and must be completed by the user.

Add custom fields to your Add-On

To add custom fields to your Add-On, you will need to specify the following for each field:

name: The name is unique among all custom fields for a given Add-On. It is also going to be the slug sent back to you as part of the resource provision payload.

display_name: Human readable name of configurable variable. DigitalOcean will use the name in the Add-On sign-up form.

description: A description of the configurable variable

type: The data type of the expected input value. Currently we support boolean and text string inputs.

options: Options are a list of possible responses for a string custom field. If you provide options, the string input will be changed to a select field with corresponding options.

Example -

// In the below example the user will be able to choose a value only from the given options
name: region
display name: Region
description: Select which region your resource will be created in
type: string (generates a select field)
options: nyc, sfo, blr, hyd 

// In the below example the user will be presented with a check box
name: tls
display name: TLS Enabled
description: Select if you want tls enabled
type: boolean (generates a checkbox)

// In the below example the user will be presented with a text box to enter the value
name: name
display name: Name
description: Name of the person who is creating the account
type: string (generates a text input field)

Receive metadata in resource creation payload

User-supplied metadata will be included in the resource provision payload. The “metadata” section will include the key-value representation of the metadata, where the key will be the name of the metadata and value will be the user input.

Example payload -

// POST /digitalocean/resources
// Content-Type: application/json
// Authorization: Basic YWNtZToxMjM0

{
  // User selected app slug as provided by the vendor during vendor registration
  "app_slug": "example",

  // User selected plan slug as provided by the vendor during vendor registration
  "plan_slug": "premium", 

  // DigitalOcean generated UUID for identifying this specific resource
  "uuid": "RESOURCE_UUID", 
  "callback_url": "https://api.digitalocean.com/v2/saas/RESOURCE_UUID",

  // Customizable metadata that a DigitalOcean user can set for this specific Add-On
  "metadata": { "region": "nyc", "tls": true, "name":"Sammy" },

  // An obfuscated email pointing to the user's email address. Anything sent to this
  // email address will be forwarded to the user.
  "email": "<uuid>@digitalocean.com", 

  // DigitalOcean generated UUID that will uniquely identify the user. This is useful to
  // know when the same DigitalOcean user provisions multiple resources.
  "creator_uuid": "CREATOR_UUID",

  // You'll need to asynchronously exchange this authorization_code for an access_token and 
  // refresh_token upon success. With the access_token, you can modify this specific resource
  // within DigitalOcean.
  "oauth_grant": { 
    "type": "authorization_code",
    "code": "AUTHORIZATION_CODE_UUID",
    "expires_at": 1620915831
  }
}

Note: DigitalOcean Terms of Service prohibit you from collecting email addresses through the custom fields/metadata feature.

Abusive Users

If you determine that a user from DigitalOcean is being abusive on your platform, please submit a ticket in the Marketplace Help Center with the following information:

  • The resource_uuid
  • If you have these, include: the DigitalOcean user_id that is associated with the abusive account and the creator_id.
  • A thorough description of the abuse.

Tips for Testing your Add-On

Once you have built your integration, test it and make sure everything works as expected. As a reminder, ensure any person testing your Add-On is a member of the Team associated with the Add-On in Vendor Portal. For more information, review our Team documentation. From the Vendor Portal UI, there are two ways to test resource provisioning:

Testing provisioning via the Preview button

You can find a Preview button at the bottom of your Add-On’s Create or Edit Listing page. When you select the Preview button, you’ll be shown your Add-On’s listing page in preview mode.  From here, you can test provisioning by selecting the Add your-addon button, and verifying the account was provisioned on your service.

Testing provisioning via the Plan Information tab

On the Plan Information tab within your Add-On’s listing, find the Plans section and select the more menu button to the right of the plan you want to test. Selecting Test Plan will provision a resource for that specific plan.

Note: Provisioning paid plans in this way will skip our billing logic, so you should never be charged.

Once you have successfully provisioned a resource (and matching account) on your service, you should test:

  • Plan Changes: Verify that your SaaS changes the plan and account limits for the customer
  • Single Sign-On: Verify that you can SSO to your SaaS
  • Resource Destruction: Verify that destroying the resource via DigitalOcean removes the corresponding account on your SaaS.
  • Notifications: You can test notifications by provisioning a resource, and then changing its name using the edit resources settings form.  This will elicit a resources.updated webhook to your service.