Reseller API

Provision and manage private labels from your own systems

Create private labels for your clients, sell them packages and add-ons, and read their usage and points. JSON over HTTPS, with OAuth2 client credentials.

# Exchange your API key for an access token
ACCESS_TOKEN=$(curl -s -X POST https://api-app.sign.net/api/v1/auth/token \
  -H 'Content-Type: application/json' \
  -d '{"grant_type":"client_credentials","api_key":"snk_live_..."}' \
  | jq -r .access_token)

# Find your reseller's slug
SLUG=$(curl -s https://api-app.sign.net/console/dashboard \
  -H "Authorization: Bearer $ACCESS_TOKEN" | jq -r .data.slug)

# List your private labels
curl "https://api-app.sign.net/console/$SLUG/reseller/private-labels" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Two APIs, two jobs

  • Reseller API (Provision and billing)

    Live today

    Create private labels for your clients, look after their people and branding, sell them packages and add-ons, and read their usage and points. Documented on this page.

    Get started
  • Signing API (Embed sending and signing)

    api.sign.net

    Send documents for signature and embed signing in your own product. Documented on api.sign.net.

    Read the Signing API docs

Selling from WHMCS? Our WHMCS plugin creates, suspends, changes and deletes portals with your WHMCS services, so there's no code to write.

What you can build

Four common integrations, and the calls each one makes. Paths are relative to /console/{slug}/reseller, where {slug} is your reseller's domain.

  • Auto-provision a client after checkout

    When an order is paid in your store, create the client's private label with the package they bought in one call, then add their team.

    1. POST/private-labels
    2. POST/private-labels/:tenantId/users
  • Sell tiered packages with overage

    Define Starter, Business and Pro packages at your own prices, and sell extra seats or documents as add-ons when a client goes over.

    1. POST/billing/packages
    2. POST/billing/addons
    3. POST/private-labels/:tenantId/package/addons
  • Show usage on your own dashboard

    Pull each client's monthly usage and points balance into your portal, so they see what they've used without leaving it.

    1. GET/usage
    2. GET/private-labels/:tenantId/points
    3. GET/billing/quota
  • Pause a client who hasn't paid

    When an invoice goes overdue, suspend the client's private label, and lift it when they pay. Their documents and package are kept.

    1. POST/private-labels/:tenantId/suspend
    2. POST/private-labels/:tenantId/unsuspend

How the pieces fit

Everything in the API hangs off four concepts.

  1. Reseller

    You. Sign.net gives you an allowance and a points pool.

    /billing/quota · /billing/points
  2. Private label

    One per client, with its own owner, branding and members.

    /private-labels
  3. PackageAdd-ons

    One package per private label, with any number of add-ons on top.

    /billing/packages · /billing/addons
  4. Points and quota

    Documents, seats, templates and notarisations, counted in points.

    /usage · /private-labels/:tenantId/points

Developer tools

  • Endpoint reference
  • API Playground
  • Scoped API keys
  • Rate limit docs
  • SandboxComing soon
  • OpenAPI and Postman downloadComing soon
  • SDKsComing soon
  • WebhooksComing soon
  • ChangelogComing soon
  • Status pageComing soon

Try it in the API Playground

The reseller console's API Playground builds a ready-to-run cURL command for any endpoint on this page and sends test requests with your key.

Open the API Playground

Getting started

Built on /api/v1/reseller? Those paths were retired on 1 October 2026 and now answer 404. Your key, the token exchange and the scopes are unchanged. Move each call to /console/{slug}/reseller (packages and add-ons now sit under /billing, read with …/search and …/get and changed with …/update), read results from the response's data, and expect camelCase field names.

  1. Create an API key

    In the reseller console, open Keys under API, pick the scopes your integration needs and create a key. Copy the secret straight away: it is only shown once. Every key is live, so requests made with it affect real data.

  2. Exchange it for an access token

    Post the key to /api/v1/auth/token with grant_type set to client_credentials. You get back a short-lived Bearer token, and expires_in says how many seconds it lasts.

  3. Find your slug

    Call GET /console/dashboard with the token as Authorization: Bearer. Its data.slug is your reseller's domain: the {slug} in every reseller path. It only changes if your domain does.

  4. Call the API

    Send the same token on every reseller request. Answers come wrapped: status is "OK" with the result in data, or "Err" with an error.code such as INVALID_REQUEST, usually as a 400. When the token expires, after an hour, exchange your key again.

# 1. Exchange your API key for a short-lived access token
ACCESS_TOKEN=$(curl -s -X POST https://api-app.sign.net/api/v1/auth/token \
  -H 'Content-Type: application/json' \
  -d '{"grant_type":"client_credentials","api_key":"snk_live_..."}' | jq -r .access_token)

# 2. Find the slug of the reseller your key acts for
SLUG=$(curl -s https://api-app.sign.net/console/dashboard \
  -H "Authorization: Bearer $ACCESS_TOKEN" | jq -r .data.slug)

# 3. Call the endpoint
curl "https://api-app.sign.net/console/$SLUG/reseller/private-labels" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Token request

{
  grant_type: "client_credentials"
  api_key: string
}

Token response 200

{
  access_token: string
  token_type: "Bearer"
  expires_in: number
  scope: string
}

Scopes

Each key carries the scopes you pick when you create it, and each endpoint needs one of them. A call without the right scope is refused with 403 INSUFFICIENT_SCOPE. A key can't mint or revoke keys: someone signed in to the console does that.

reseller:read
Read your dashboard, private labels, their members, templates, packages, usage, quota and points, and your own packages, add-ons and keys.
reseller:provision
Provision, suspend, unsuspend and deprovision private labels, retry their hosts, add and remove their people, resend invites, and change their colours and logos.
reseller:packages
Create and update your packages and add-ons, and give them to private labels or take them off. Provisioning a label with a package needs this as well as reseller:provision.

Rate limits

Billing routes and the rest of the console have separate budgets, so a burst of label reads doesn't hold back a package assignment. A call that spends your allowance also counts against Allocation: assigning a package or attaching an add-on there and in Billing, provisioning there and in Console.

LimitApplies toCounted perCeiling
Token exchangeExchanging your API key for an access tokenYour IP address5 every 15 minutes
BillingEvery billing/* route, and a private label's package and points routesYour IP address60 a minute
ConsoleEvery other route a key can call: labels, people, usage, branding, dashboardYour IP address60 a minute
AllocationAssigning a package, attaching an add-on, and provisioning with a packageYour reseller, across all its keys20 a minute

Limited responses carry RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset. Going over answers 429 with a Retry-After header and a plain { "error": "…" } body rather than the usual envelope. Behind all of these, one address can make 300 calls a minute in total. You can hold up to 100 packages and 100 add-ons.

Building your integration

The conventions every endpoint follows, where to call it from, how to test without a sandbox, the path from an order to a live portal, and how to keep your records in step.

Conventions

Formats

Requests
JSON with Content-Type: application/json, except logo uploads, which are multipart/form-data sent to https://api.sign8.net. Field names are camelCase both ways.
Ids
32 lowercase hex characters, such as a tenantId, packageId or userId. Store them as strings.
Times
Milliseconds since the Unix epoch, such as createdAt and expiresAt.
Months
A period is a calendar month in UTC, written YYYY-MM. Without one, you get the current month.
Money
Whole minor units, such as cents, beside a three-letter ISO currency such as SGD. Your prices are for you and your clients: Sign.net never charges with them.
Codes
A package or add-on code is stored upper case and is unique among yours, so growth and GROWTH are the same code.
Hosts
A domain or primaryHost is a bare hostname, such as sign.acmelegal.com, with no https:// or path.

What each item counts

documents
Documents sent in the period.
notarizations
Documents whose notarisation landed on chain in the period, whatever happened to them afterwards.
seats
People holding a seat on a label. Someone who was only sent a document to sign holds none.
templates
Templates a label holds.
points
What a label's people used from its points pool, less what they gave back. Measured, never billed.

The billed figures in GET …/billing/quota can read more than GET …/usage. Each label is billed the greater of what you allocated it and what it used, and your own tenant what it used, while usage counts only what was used.

Call it from your server

Browsers may only call the API from Sign.net's own sites and the portals it hosts, so a browser refuses a call from a page on your own website. That keeps your key where it belongs: anyone who can open a page or an app that holds it can read it.

  • Keep the key in your server's secrets, such as an environment variable or a secrets manager. Never put it in your repository, or in anything you ship to a browser or phone.
  • Keep one access token where all your server's workers can reach it, so a restart or a new worker reuses it rather than spending the token exchange's budget.
  • Have your own pages and apps ask your server, and let your server call Sign.net.

Testing without a sandbox

There's no sandbox yet. Every key acts on your live reseller, and the API Playground sends real requests too, so build against your own account knowing which calls cost nothing and which are real.

Free to try as often as you like

  • Reading anything

    Every GET, and package and add-on search. Build your reads with a key that holds only reseller:read, and it can't change anything.

  • Suspending and unsuspending

    Signs a label's people out and lets them back in, with nothing on the label lost.

  • Changing colours and logos

    Send the old ones back to undo it.

Real, and counted against your allowance

  • Provisioning a label

    Creates a real portal and emails its owner, so use an owner address you control. Its seats and documents count against your allowance like any other label's, suspended or not, until you deprovision it.

  • Assigning a package or attaching an add-on

    Counts for the whole billing period, at the greater of what you allocated and what the label used, even if you take it off the same day. Test with the smallest quantities you can.

  • Deprovisioning a label

    Permanent, and its host can never be provisioned again, so give test labels hosts you won't want later, such as test-1.yourdomain.com.

  • Adding people and sending invites

    Emails each person a link to set a password. Use addresses you control.

  • Creating packages and add-ons

    They stay. Deactivating one keeps its code and its place among your 100, so give test ones a code you'll recognise, such as TEST-STARTER.

From order to live portal

The calls an integration makes between a client paying and their portal opening.

  1. Provision the label with POST …/private-labels, sending the owner, the config (its domain, appName and colours) and the package they bought. Store the tenantId and ownerUserId it answers with.

  2. Finish anything that didn't happen. If package.outcome isn't Assigned, complete the assignment as Going over your allowance describes. If domainSetup.attached is false, retry with POST …/private-labels/:tenantId/domain/attach.

  3. Give your customer the DNS record: the first entry in domainSetup.records, usually a CNAME from their host to cname.vercel-dns.com, or an A record for a whole domain. Any further entries are alternatives for the same name, and a name takes only one.

  4. Wait for DNS. Read GET …/private-labels/:tenantId until domainSetup.verified is true. It's refreshed at most once a minute and DNS can take hours to spread, so every few minutes is plenty.

  5. Make sure the owner can sign in. Their welcome email gives them the same record, then a link to set their password that lasts a day. If DNS took longer, send a fresh one with POST …/private-labels/:tenantId/users/:userId/invite, using the ownerUserId.

  6. Add their team with POST …/private-labels/:tenantId/users. Each person is emailed a link to set a password.

Record each order before you call, so a lost answer sends you looking for the host in GET …/private-labels rather than provisioning twice.

Keeping in sync

There are no webhooks yet, so read back what can change without a call of yours. How often is a suggestion, not a limit.

What changesWhere to read itHow often
A label's DNS goes livedomainSetup.verified from GET …/private-labels/:tenantIdEvery few minutes, until it reads true
Sign.net suspends a label, or moves it to another resellerstatus and suspendedBy from GET …/private-labels, or the label missing from itHourly: one call covers every label
A label's admin adds people in their portalGET …/private-labels/:tenantId/membersDaily, or before you bill a client for seats
Your labels send documents, notarise them and take seatsGET …/usage?period=YYYY-MMHourly is plenty for a dashboard
Your remaining allowanceGET …/billing/quotaBefore each assignment
Points pools renew and lapseGET …/billing/pointsHourly: renewals and lapses are written once an hour

Spread per-label reads out. From one address, the Console budget allows 60 calls a minute and the Billing budget, which includes a label's package and points, another 60.

Working with the API

How calls fail and what to do about each, how to go over your allowance on purpose, and which calls are safe to send again.

Status codes and errors

Nearly every answer comes in the same envelope: status reads "OK" with the result in data, or "Err" with an error.code and error.message. The exceptions are under the table.

StatusCodeWhat it meansWhat to do
200—It worked, and data holds the result. Assigning a package or attaching an add-on can also answer 200 with outcome: "ConfirmationRequired", having written nothing.Read data. On those two calls, check outcome as well.
400INVALID_REQUEST, INVALID_TARGET, HOST_TAKEN, NOT_ASSIGNED, …The request broke a rule or didn't decode. Each endpoint below lists the codes it can send.Fix the request. Sent again unchanged, it fails the same way.
401UNAUTHORIZEDThe access token is missing, expired or invalid, or its key was revoked.Exchange your key for a new token, then send the call once more.
403INSUFFICIENT_SCOPEYour key doesn't hold the scope the endpoint needs.Mint a key with that scope. A key's scopes can't be changed later.
403FORBIDDENThe slug isn't your key's reseller, or the private label isn't one of yours, which includes one you've deprovisioned.Check the slug against GET /console/dashboard and the tenantId against your list of private labels.
403SESSION_REQUIREDOnly a person signed in to the console can do this, such as minting or revoking a key.Do it in the console.
404NOT_FOUNDNo endpoint answers that method and path, such as a retired /api/v1/reseller path.Check the method and path against the endpoints below.
429{ "error": "…" }, TOO_FREQUENTYou went over a rate limit, or asked to invite someone who was sent a set-password email in the last five minutes.Wait the number of seconds in Retry-After, then send it again.
500INTERNAL_SERVER_ERRORSomething failed on our side, and status reads "Fail". An invite whose email couldn't be sent answers 500 too.Retry with a growing delay if the call is safe to repeat (below), and check what happened first if it isn't. Tell us if it keeps happening.
  • Branch on error.code, not error.message: the message is written for people and may change.
  • Two kinds of answer skip the envelope. The token exchange answers in OAuth2's format: { "error": "invalid_request" } (400, a missing or wrong grant_type or api_key) or { "error": "invalid_client" } (401, a key that is unknown, revoked or mistyped). A rate limit's 429 is a bare { "error": "…" }.
  • On the flat /console/* endpoints, a call with no token, or a malformed one, answers 400 JSON_WEB_TOKEN_ERROR, and 400 UNAUTHORIZED means the domain you sent isn't one of your private labels.

Going over your allowance

Assigning a package or attaching an add-on spends your Sign.net allowance straight away, whether or not the label uses it. Going past your allowance is allowed, but never by accident: the first call writes nothing and asks you to confirm.

  1. Send the assignment as usual: POST …/private-labels/:tenantId/package with packageId, or …/package/addons with addonId and quantity.

  2. If it fits, it's done, and outcome reads Assigned or Attached. If it doesn't, outcome reads ConfirmationRequired and nothing has been written. Each warning gives an item, how much you asked for, how much you have left and how far over you'd go.

  3. To go ahead, send the identical body again with confirmationKey set to confirmation.key within 10 minutes. expiresAt is when it runs out, in milliseconds.

Over your allowance 200

{
  "status": "OK",
  "data": {
    "outcome": "ConfirmationRequired",
    "confirmation": {
      "key": "…",
      "expiresAt": 1758800000000
    },
    "warnings": [
      {
        "itemCode": "seats",
        "requested": 10,
        "remaining": 3,
        "excess": 7
      }
    ]
  }
}
  • A key belongs to the request it was issued for: the same label, the same package or add-on and the same quantity. It is only checked when a request would take you over: sent with a different one that would also go over, it answers 400 CONFIRMATION_INVALID, and with one that fits, it is ignored. A key over 10 minutes old answers 400 CONFIRMATION_EXPIRED, so send the request without it for a new one.
  • If the numbers moved in between, because you edited the package or another assignment used up allowance, you get ConfirmationRequired again with the new warnings and a fresh key, and still nothing is written.
  • Assignments sent at the same moment are checked one after another, so two that each fit on their own can't both go through unconfirmed.
  • Provisioning with a package can't take a key, because the label didn't exist yet. Leave confirmationKey out. If package.outcome reads ConfirmationRequired, the label was created without its package: send the package to …/private-labels/:tenantId/package with the new tenantId and the key it gave you.
  • To see what you have left before you assign, read GET …/billing/quota: each item's remaining.

Safe retries

Sending the same request again won't clear a 400 or a 403, so fix it first. A 429 clears after Retry-After. When an answer is lost to a timeout, a dropped connection or a 500, what to do depends on the call. Some answer an error the second time, which can simply mean the first one worked.

Sending again succeeds

  • Suspend or unsuspend a private label

    Answers the label's current state, whatever it was before. A label Sign.net suspended can't be unsuspended from here: that answers 400 SUSPENDED_BY_PLATFORM, however often you try.

  • Attach a label's host again

    A host that's already attached answers as attached.

  • Add someone with POST …/private-labels/:tenantId/users

    Someone already on the label answers 200 again, even at its seat cap, but with status: "added" and created: false where the first answer said "created".

  • Remove someone with DELETE …/private-labels/:tenantId/users/:userId

    Someone already removed answers "removed" again.

  • Change colours, logos, a package or an add-on

    Sets the same values again.

Sending again answers an error

  • Provision a private label

    Its host is taken once it exists, so a second try answers HOST_TAKEN. Look for the host in GET …/private-labels before trying again.

  • Assign a package or attach an add-on

    A second assignment answers ALREADY_ASSIGNED. A second attachment answers ALREADY_ATTACHED, or ConfirmationRequired if the first used up the room it needed, so don't confirm it before reading GET …/private-labels/:tenantId/package.

  • Add someone with POST /console/users/add

    A second try answers 400 ALREADY_IN_DOMAIN: they're already on the label.

  • Create a package or add-on

    A second try answers CODE_IN_USE. Find the first one with …/search.

  • Remove a package or add-on

    A second try answers NOT_ASSIGNED or ADDON_NOT_FOUND: it's already gone.

  • Send a fresh invite

    Refused with 429 TOO_FREQUENT and Retry-After for five minutes after any set-password email to that person, the one adding them sent included. A 500 means the email wasn't sent, and the five minutes still start.

  • Deprovision a private label

    Permanent: its host can never be used again, and a second try answers 403 FORBIDDEN. To cut a client off for now, suspend the label instead.

Provisioning answers 200 once the label exists, even if part of the rest didn't happen. Check domainSetup.attached before telling a client their portal is live, and retry with …/domain/attach if it reads false. Check package.outcome too: Failed comes with the assignment's error, such as NO_SUBSCRIPTION or PACKAGE_INACTIVE. Fix what it names, then assign the package with …/package.

Keys and tokens

  • A key is shown once

    It looks like snk_live_<id>_<secret> and is shown only when it's minted. Keep it with your other secrets, never in code or a browser. It acts for one reseller, the one whose console minted it.

  • Reuse each token for its hour

    A token lasts expires_in seconds, an hour. The token exchange allows 5 every 15 minutes from one address, so exchanging the key on every call soon answers 429. Keep the token, and exchange again when it expires or a call answers 401.

  • Rotate without downtime

    Mint the new key in the console, deploy it, then revoke the old one. Revoking a key ends its tokens at once, not when they expire.

  • Give a key only what it needs

    Scopes are fixed when a key is minted. A key that only reads usage needs only reseller:read. Keys can't mint or revoke keys, so a leaked key can't issue itself a successor.

Paging

  • Package and add-on searches page in the body: { query, page, pageSize }, all three required. page counts from 1, pageSize runs from 1 to 100, and a query of "" lists everything. The answer carries total, so the last page is the one where page × pageSize reaches it.
  • Points ledgers page in the query: ?page=&pageSize=, newest first, with total beside the rows. Leave them out for the first 20 rows; pageSize goes up to 100. itemCode narrows them to one item. A value out of range answers 400 INVALID_QUERY.
  • Rows written while you page push older ones down, so one can turn up on two pages. Each has an id to tell them apart.
  • People search answers at most 50 matches and has no second page. Make the query narrower to reach someone past them.
  • Your private labels, and a label's members, come back whole.

Getting help with a failed call

Apart from the token exchange, answers carry no request id, so we find a call by when it was made and which key made it. Contact us with:

  • When it happened, to the minute, with the time zone.
  • The method and the full path you called, ids included.
  • Your key's id: the 32 characters after snk_live_, which aren't secret. Never send the part after them, or a token.
  • The status and error.code you got back, and the body you sent, less any personal details you don't need to share.
  • For the token exchange, the X-Request-Id header of its answer: it is the one call that carries one.

Endpoints

Every path below is relative to https://api-app.sign.net, except logo uploads, which go to https://api.sign8.net. Replace :slug with your slug. Open an endpoint to see what it takes, what it returns and how it fails. Ids are 32-character hex strings, timestamps are milliseconds since the epoch and prices are in minor units, such as cents.

Dashboard and people

Find out which reseller your key acts for, and seat or remove people on the hosts you run.

GETDashboard/console/dashboardreseller:read

The private labels you administer, with how many people each seats. slug is the reseller you act for — the :slug in the reseller endpoints. A key sees its own reseller's labels only.

Response 200on success

{
  status: "OK"
  data: {
    totalDomains: number
    totalUsers: number
    domains: { domain: string, companyName: string, userCount: number }[]
    isReseller: boolean
    slug: string | null
  }
}

Errors

  • 401
    { "status": "Err", "error": { "code": "UNAUTHORIZED", "message": "…" } }

    Missing, expired or revoked access token — exchange your API key again

  • 403
    { "status": "Err", "error": { "code": "INSUFFICIENT_SCOPE", "message": "…" } }

    Your key does not hold the scope above

  • 403
    { "status": "Err", "error": { "code": "FORBIDDEN", "message": "…" } }

    The reseller your key was minted for has been deleted

  • 429
    { "error": "Too many requests, please try again later." }

    More than 60 calls a minute from your address — wait for the Retry-After header

POSTSearch people/console/users/searchreseller:read

Finds people on the hosts you administer by email or name, up to 50. domain narrows the search to one host, and an empty query then lists everyone on it. A signed-in admin also searches the reseller's own staff; a key searches its private labels only.

Parameters

  • querystringOptional

    Search (empty for everyone)

  • domainstring (a host you administer)Optional

    Only this host

Response 200on success

{
  status: "OK"
  data: {
    users: { id: string, firstName: string, lastName: string, email: string, status: string, domain: string }[]
  }
}

Errors

  • 400
    { "status": "Err", "error": { "code": "INVALID_REQUEST", "message": "…" } }

    A body that does not decode — a missing or mistyped field

  • 401
    { "status": "Err", "error": { "code": "UNAUTHORIZED", "message": "…" } }

    Missing, expired or revoked access token — exchange your API key again

  • 403
    { "status": "Err", "error": { "code": "INSUFFICIENT_SCOPE", "message": "…" } }

    Your key does not hold the scope above

  • 403
    { "status": "Err", "error": { "code": "FORBIDDEN", "message": "…" } }

    The reseller your key was minted for has been deleted

  • 429
    { "error": "Too many requests, please try again later." }

    More than 60 calls a minute from your address — wait for the Retry-After header

POSTAdd a person to a host/console/users/addreseller:provision

Seats someone on a host you administer, creating their account if needed; they are emailed a link to set a password.

Parameters

  • emailstringRequired

    Email

  • firstNamestringRequired

    First name

  • lastNamestringRequired

    Last name

  • domainstring (a host you administer)Required

    Domain

Response 200on success

{
  status: "OK"
  data: {
    message: string
  }
}

Errors

  • 400
    { "status": "Err", "error": { "code": "UNAUTHORIZED", "message": "…" } }

    A domain you do not administer. A key administers its reseller's private labels only, not the reseller's own host

  • 400
    { "status": "Err", "error": { "code": "DOMAIN_NOT_FOUND", "message": "…" } }

    No live tenant has that host

  • 400
    { "status": "Err", "error": { "code": "ALREADY_IN_DOMAIN", "message": "…" } }

    They already hold a seat there

  • 400
    { "status": "Err", "error": { "code": "SEAT_QUOTA_REACHED", "message": "…" } }

    The host has used every seat its package grants

  • 400
    { "status": "Err", "error": { "code": "INVALID_EMAIL", "message": "…" } }

    Not one plain email address — refused before any account exists

  • 400
    { "status": "Err", "error": { "code": "INVALID_NAME", "message": "…" } }

    A blank first or last name

  • 400
    { "status": "Err", "error": { "code": "INVALID_REQUEST", "message": "…" } }

    A body that does not decode — a missing or mistyped field

  • 401
    { "status": "Err", "error": { "code": "UNAUTHORIZED", "message": "…" } }

    Missing, expired or revoked access token — exchange your API key again

  • 403
    { "status": "Err", "error": { "code": "INSUFFICIENT_SCOPE", "message": "…" } }

    Your key does not hold the scope above

  • 403
    { "status": "Err", "error": { "code": "FORBIDDEN", "message": "…" } }

    The reseller your key was minted for has been deleted

  • 429
    { "error": "Too many requests, please try again later." }

    More than 60 calls a minute from your address — wait for the Retry-After header

POSTRemove a person from a host/console/users/removereseller:provision

Takes someone's seat on a host away and signs them out at once. Their account is kept, so adding them again seats it again, without any admin rights they held. Owners and admins cannot be removed, whether with an API key or signed in.

Parameters

  • emailstringRequired

    Email

  • domainstring (a host you administer)Required

    Domain

Response 200on success

{
  status: "OK"
  data: {
    message: string
  }
}

Errors

  • 400
    { "status": "Err", "error": { "code": "UNAUTHORIZED", "message": "…" } }

    A domain you do not administer. A key administers its reseller's private labels only, not the reseller's own host

  • 400
    { "status": "Err", "error": { "code": "USER_NOT_FOUND", "message": "…" } }

    Nobody with that email is on that host

  • 400
    { "status": "Err", "error": { "code": "CANNOT_REMOVE_OWNER", "message": "…" } }

    That is the owner or another of the admins here

  • 400
    { "status": "Err", "error": { "code": "INVALID_REQUEST", "message": "…" } }

    A body that does not decode — a missing or mistyped field

  • 401
    { "status": "Err", "error": { "code": "UNAUTHORIZED", "message": "…" } }

    Missing, expired or revoked access token — exchange your API key again

  • 403
    { "status": "Err", "error": { "code": "INSUFFICIENT_SCOPE", "message": "…" } }

    Your key does not hold the scope above

  • 403
    { "status": "Err", "error": { "code": "FORBIDDEN", "message": "…" } }

    The reseller your key was minted for has been deleted

  • 429
    { "error": "Too many requests, please try again later." }

    More than 60 calls a minute from your address — wait for the Retry-After header

Branding

Change a private label's colours and logos.

POSTChange a host's colours/console/tenant/update-colorsreseller:provision

Changes a host's colours. Send only the ones to change; one left blank keeps its current colour. Colours are hex, #rgb or #rrggbb.

Parameters

  • domainstring (a host you administer)Required

    Domain

  • colorPrimarystring (hex)Optional

    Primary

  • colorPrimaryForegroundstring (hex)Optional

    Text on primary

  • colorAccentstring (hex)Optional

    Accent

  • colorAccentForegroundstring (hex)Optional

    Text on accent

  • colorAppBarBackgroundstring (hex)Optional

    App bar

  • colorFooterBackgroundstring (hex)Optional

    Footer

Response 200on success

{
  status: "OK"
  data: {
    domain: string
    message: string
  }
}

Errors

  • 400
    { "status": "Err", "error": { "code": "DOMAIN_EMPTY", "message": "…" } }

    Blank domain

  • 400
    { "status": "Err", "error": { "code": "UNAUTHORIZED", "message": "…" } }

    A domain you do not administer. A key administers its reseller's private labels only, not the reseller's own host

  • 400
    { "status": "Err", "error": { "code": "NOT_FOUND", "message": "…" } }

    The host has no branding to change

  • 400
    { "status": "Err", "error": { "code": "INVALID_COLOR", "message": "…" } }

    A colour that is not hex, #rgb or #rrggbb

  • 400
    { "status": "Err", "error": { "code": "INVALID_REQUEST", "message": "…" } }

    A body that does not decode — a missing or mistyped field

  • 401
    { "status": "Err", "error": { "code": "UNAUTHORIZED", "message": "…" } }

    Missing, expired or revoked access token — exchange your API key again

  • 403
    { "status": "Err", "error": { "code": "INSUFFICIENT_SCOPE", "message": "…" } }

    Your key does not hold the scope above

  • 403
    { "status": "Err", "error": { "code": "FORBIDDEN", "message": "…" } }

    The reseller your key was minted for has been deleted

  • 429
    { "error": "Too many requests, please try again later." }

    More than 60 calls a minute from your address — wait for the Retry-After header

POSTUpload a host's logos/console/tenant/upload-logosreseller:provision

Replaces a host's logo, its banner logo, or both. The body is multipart: the host goes in a data part as JSON, beside the image parts. Images are resized (the logo to fit 270×90, the banner to 500×500) and stored as PNG. Both are read before either is saved, so an image that cannot be read leaves the current logos as they are.

Send it as multipart/form-data to https://api.sign8.net rather than the usual API host: uploads to the usual host are blocked at its edge.

Parameters

  • data.domainstring (a host you administer)Required

    Domain

  • logo (file part)PNG, JPEG, GIF or SVG, up to 1 MBOptional

    Logo

  • bannerLogo (file part)PNG, JPEG, GIF or SVG, up to 1 MBOptional

    Banner logo

Response 200on success

{
  status: "OK"
  data: {
    message: string
  }
}

Errors

  • 400
    { "status": "Err", "error": { "code": "NO_FILES", "message": "…" } }

    Neither a logo nor a banner logo was attached

  • 400
    { "status": "Err", "error": { "code": "DOMAIN_EMPTY", "message": "…" } }

    Blank domain

  • 400
    { "status": "Err", "error": { "code": "INVALID_DATA", "message": "…" } }

    The data part is missing or is not JSON with a domain

  • 400
    { "status": "Err", "error": { "code": "DOMAIN_INVALID", "message": "…" } }

    A domain that is not a host name

  • 400
    { "status": "Err", "error": { "code": "INVALID_IMAGE", "message": "…" } }

    An image that cannot be read, or one larger than 4096 × 4096 pixels

  • 400
    { "status": "Err", "error": { "code": "UNAUTHORIZED", "message": "…" } }

    A domain you do not administer. A key administers its reseller's private labels only, not the reseller's own host

  • 400
    { "status": "Err", "error": { "code": "LIMIT_FILE_SIZE", "message": "…" } }

    An image over 1 MB

  • 400
    { "status": "Err", "error": { "code": "LIMIT_UNEXPECTED_FILE", "message": "…" } }

    An image that is not PNG, JPEG, GIF or SVG, or a part it does not take

  • 401
    { "status": "Err", "error": { "code": "UNAUTHORIZED", "message": "…" } }

    Missing, expired or revoked access token — exchange your API key again

  • 403
    { "status": "Err", "error": { "code": "INSUFFICIENT_SCOPE", "message": "…" } }

    Your key does not hold the scope above

  • 403
    { "status": "Err", "error": { "code": "FORBIDDEN", "message": "…" } }

    The reseller your key was minted for has been deleted

  • 429
    { "error": "Too many requests, please try again later." }

    More than 60 calls a minute from your address — wait for the Retry-After header

Private labels

Provision a private label with its owner, branding and package, read it back, retry its host, suspend or unsuspend it, and deprovision it.

GETList private labels/console/:slug/reseller/private-labelsreseller:read

Every private label you provisioned, with its owner, package, template allowance, suspension and this period's counts. period is a month or lifetime, and defaults to this month.

Parameters

  • :slugstring (your primaryHost)Required

    Your reseller's domain

  • ?period"YYYY-MM" | "lifetime"Optional

    Period

Response 200on success

{
  status: "OK"
  data: {
    privateLabels: {
      tenantId: string
      primaryHost: string
      customDomain: string | null
      adminEmail: string | null
      assignedPackage: { packageId: string, code: string, name: string } | null
      templateQuota: { used: number, total: number, unlimited: boolean }
      metrics: { templates: number, documentsSent: number, members: number }
      status: "Active" | "Suspended"
      suspendedAt: number | null
      suspendedBy: "Platform" | "Reseller" | null
      suspendReason: string | null
    }[]
    provisionedCount: number
    period: string
  }
}

Errors

  • 401
    { "status": "Err", "error": { "code": "UNAUTHORIZED", "message": "…" } }

    Missing, expired or revoked access token — exchange your API key again

  • 403
    { "status": "Err", "error": { "code": "INSUFFICIENT_SCOPE", "message": "…" } }

    Your key does not hold the scope above

  • 403
    { "status": "Err", "error": { "code": "FORBIDDEN", "message": "…" } }

    The slug is not your key's reseller, or the target is outside your subtree

  • 429
    { "error": "Too many requests, please try again later." }

    More than 60 calls a minute from your address — wait for the Retry-After header

POSTProvision a private label/console/:slug/reseller/private-labelsreseller:provision

Creates a private label under you, seats its owner (who is emailed a link to set a password) and attaches its host. It can also give the label one of your packages in the same call — with a key, that needs reseller:packages as well. The label is created first, so a refused assignment still leaves a working label, and package says what happened. Colours must be hex and links full http(s):// URLs, and a label cannot take one of sign.net's own hosts; all of that is checked before anything is created.

Parameters

  • :slugstring (your primaryHost)Required

    Your reseller's domain

  • owner.emailstringRequired

    Owner email

  • owner.firstNamestringRequired

    Owner first name

  • owner.lastNamestringRequired

    Owner last name

  • config.domainstring (hostname)Required

    Domain

  • config.appNamestringRequired

    App name

  • config.supportUrlstring (URL)Optional

    Support URL

  • config.websiteUrlstring (URL)Optional

    Website URL

  • config.colorPrimarystring (hex)Optional

    Primary color

  • config.colorAccentstring (hex)Optional

    Accent color

  • package.packageIdstringOptional

    Package to assign

  • package.confirmationKeystringOptional

    Package confirmation key (only to go over your allowance)

Response 200on success

{
  status: "OK"
  data: {
    tenantId: string
    ownerUserId: string
    domainSetup: { attached: boolean, verified: boolean, records: { type: string, name: string, value: string }[], note?: string }
    package?:
      | { outcome: "Assigned", assignmentId: string, allocated: { documents: number, seats: number, templates: number, notarizations: number } }
      | { outcome: "ConfirmationRequired", confirmation: { key: string, expiresAt: number }, warnings: { itemCode: "documents" | "seats" | "templates" | "notarizations", requested: number, remaining: number, excess: number }[] }
      | { outcome: "Failed", error: string }
  }
}

Errors

  • 400
    { "status": "Err", "error": { "code": "HOST_TAKEN", "message": "…" } }

    That config.domain is already in use

  • 400
    { "status": "Err", "error": { "code": "DOMAIN_INVALID", "message": "…" } }

    A config.domain that is not a hostname — not an email address or URL

  • 400
    { "status": "Err", "error": { "code": "HOST_RESERVED", "message": "…" } }

    One of sign.net's own hosts, such as its app, console, admin, API or static host

  • 400
    { "status": "Err", "error": { "code": "INVALID_COLOR", "message": "…" } }

    A colour that is not hex, #rgb or #rrggbb

  • 400
    { "status": "Err", "error": { "code": "INVALID_URL", "message": "…" } }

    A websiteUrl, supportUrl or social link that is not a full http:// or https:// URL

  • 400
    { "status": "Err", "error": { "code": "TOO_MANY_SOCIAL_LINKS", "message": "…" } }

    More than 20 social links

  • 400
    { "status": "Err", "error": { "code": "OWNER_INVALID_EMAIL", "message": "…" } }

    The owner's email is not an email address

  • 400
    { "status": "Err", "error": { "code": "OWNER_INVALID_NAME", "message": "…" } }

    The owner's first or last name is blank

  • 400
    { "status": "Err", "error": { "code": "INVALID_REQUEST", "message": "…" } }

    Malformed body, a blank appName, or a value longer than its field

  • 403
    { "status": "Err", "error": { "code": "INSUFFICIENT_SCOPE", "message": "…" } }

    A key sending a package without reseller:packages — refused before anything is created

  • 401
    { "status": "Err", "error": { "code": "UNAUTHORIZED", "message": "…" } }

    Missing, expired or revoked access token — exchange your API key again

  • 403
    { "status": "Err", "error": { "code": "FORBIDDEN", "message": "…" } }

    The slug is not your key's reseller, or the target is outside your subtree

  • 429
    { "error": "Too many requests, please try again later." }

    More than 60 calls a minute from your address — wait for the Retry-After header

GETGet a private label/console/:slug/reseller/private-labels/:tenantIdreseller:read

One private label as a billing system shows it: whether it is suspended, who owns it, how many seats it holds, its package, and whether its host is served and its DNS is right (read live).

Parameters

  • :slugstring (your primaryHost)Required

    Your reseller's domain

  • :tenantIdstring (32-char hex)Required

    Private label tenant id

Response 200on success

{
  status: "OK"
  data: {
    tenantId: string
    primaryHost: string
    customDomain: string | null
    createdAt: number
    status: "Active" | "Suspended"
    suspendedAt: number | null
    suspendedBy: "Platform" | "Reseller" | null
    suspendReason: string | null
    owner: { userId: string, email: string, firstName: string, lastName: string } | null
    memberCount: number
    assignment: {
      id: string
      packageId: string
      packageCode: string
      packageName: string
      currency: string
      assignedAt: number
      addons: { subscriptionAddonId: string, addonId: string, code: string, name: string, quantity: number }[]
      allocated: { documents: number, seats: number, templates: number, notarizations: number }
    } | null
    domainSetup: { attached: boolean, verified: boolean, records: { type: string, name: string, value: string }[], note?: string }
  }
}

Errors

  • 400
    { "status": "Err", "error": { "code": "INVALID_TARGET", "message": "…" } }

    The tenant id is not a live private label

  • 401
    { "status": "Err", "error": { "code": "UNAUTHORIZED", "message": "…" } }

    Missing, expired or revoked access token — exchange your API key again

  • 403
    { "status": "Err", "error": { "code": "INSUFFICIENT_SCOPE", "message": "…" } }

    Your key does not hold the scope above

  • 403
    { "status": "Err", "error": { "code": "FORBIDDEN", "message": "…" } }

    The slug is not your key's reseller, or the target is outside your subtree

  • 429
    { "error": "Too many requests, please try again later." }

    More than 60 calls a minute from your address — wait for the Retry-After header

POSTRetry attaching a private label's host/console/:slug/reseller/private-labels/:tenantId/domain/attachreseller:provision

Attaches a private label's host to the portal again. Provisioning attaches it without failing the provision, so a label can exist with a host that resolves nowhere (attached: false); this is the retry, and an attached host answers as attached.

Parameters

  • :slugstring (your primaryHost)Required

    Your reseller's domain

  • :tenantIdstring (32-char hex)Required

    Private label tenant id

Response 200on success

{
  status: "OK"
  data: {
    domainSetup: { attached: boolean, verified: boolean, records: { type: string, name: string, value: string }[], note?: string }
  }
}

Errors

  • 400
    { "status": "Err", "error": { "code": "INVALID_TARGET", "message": "…" } }

    The tenant id is not a live private label

  • 401
    { "status": "Err", "error": { "code": "UNAUTHORIZED", "message": "…" } }

    Missing, expired or revoked access token — exchange your API key again

  • 403
    { "status": "Err", "error": { "code": "INSUFFICIENT_SCOPE", "message": "…" } }

    Your key does not hold the scope above

  • 403
    { "status": "Err", "error": { "code": "FORBIDDEN", "message": "…" } }

    The slug is not your key's reseller, or the target is outside your subtree

  • 429
    { "error": "Too many requests, please try again later." }

    More than 60 calls a minute from your address — wait for the Retry-After header

DELETEDeprovision a private label/console/:slug/reseller/private-labels/:tenantIdreseller:provision

Deletes a private label. It then leaves your subtree, so deleting it again answers 403.

Parameters

  • :slugstring (your primaryHost)Required

    Your reseller's domain

  • :tenantIdstring (32-char hex)Required

    Private label tenant id

Response 200on success

{
  status: "OK"
  data: {
    status: "deprovisioned"
  }
}

Errors

  • 401
    { "status": "Err", "error": { "code": "UNAUTHORIZED", "message": "…" } }

    Missing, expired or revoked access token — exchange your API key again

  • 403
    { "status": "Err", "error": { "code": "INSUFFICIENT_SCOPE", "message": "…" } }

    Your key does not hold the scope above

  • 403
    { "status": "Err", "error": { "code": "FORBIDDEN", "message": "…" } }

    The slug is not your key's reseller, or the target is outside your subtree

  • 429
    { "error": "Too many requests, please try again later." }

    More than 60 calls a minute from your address — wait for the Retry-After header

POSTSuspend a private label/console/:slug/reseller/private-labels/:tenantId/suspendreseller:provision

Signs every user of the label out and refuses sign-in until it is unsuspended. Its data and package are kept, and it is still billed. Suspending one already suspended keeps that suspension — suspendedBy is "Platform" when sign.net made it.

Parameters

  • :slugstring (your primaryHost)Required

    Your reseller's domain

  • :tenantIdstring (32-char hex)Required

    Private label tenant id

  • reasonstring (up to 500 characters)Optional

    Reason (only you and sign.net see it)

Response 200on success

{
  status: "OK"
  data: {
    tenantId: string
    status: "Suspended"
    suspendedAt: number
    suspendedBy: "Reseller" | "Platform"
    suspendReason: string | null
  }
}

Errors

  • 400
    { "status": "Err", "error": { "code": "INVALID_TARGET", "message": "…" } }

    The tenant id is not a live private label

  • 400
    { "status": "Err", "error": { "code": "INVALID_REQUEST", "message": "…" } }

    A reason over 500 characters

  • 401
    { "status": "Err", "error": { "code": "UNAUTHORIZED", "message": "…" } }

    Missing, expired or revoked access token — exchange your API key again

  • 403
    { "status": "Err", "error": { "code": "INSUFFICIENT_SCOPE", "message": "…" } }

    Your key does not hold the scope above

  • 403
    { "status": "Err", "error": { "code": "FORBIDDEN", "message": "…" } }

    The slug is not your key's reseller, or the target is outside your subtree

  • 429
    { "error": "Too many requests, please try again later." }

    More than 60 calls a minute from your address — wait for the Retry-After header

POSTUnsuspend a private label/console/:slug/reseller/private-labels/:tenantId/unsuspendreseller:provision

Lifts your suspension of a private label. Unsuspending one that is not suspended changes nothing and still succeeds.

Parameters

  • :slugstring (your primaryHost)Required

    Your reseller's domain

  • :tenantIdstring (32-char hex)Required

    Private label tenant id

Response 200on success

{
  status: "OK"
  data: {
    tenantId: string
    status: "Active"
    suspendedAt: null
    suspendedBy: null
    suspendReason: null
  }
}

Errors

  • 400
    { "status": "Err", "error": { "code": "SUSPENDED_BY_PLATFORM", "message": "…" } }

    sign.net suspended it, and only sign.net can lift that

  • 400
    { "status": "Err", "error": { "code": "INVALID_TARGET", "message": "…" } }

    The tenant id is not a live private label

  • 401
    { "status": "Err", "error": { "code": "UNAUTHORIZED", "message": "…" } }

    Missing, expired or revoked access token — exchange your API key again

  • 403
    { "status": "Err", "error": { "code": "INSUFFICIENT_SCOPE", "message": "…" } }

    Your key does not hold the scope above

  • 403
    { "status": "Err", "error": { "code": "FORBIDDEN", "message": "…" } }

    The slug is not your key's reseller, or the target is outside your subtree

  • 429
    { "error": "Too many requests, please try again later." }

    More than 60 calls a minute from your address — wait for the Retry-After header

GETA private label's templates/console/:slug/reseller/private-labels/:tenantId/templatesreseller:read

The templates a private label's members made in its portal — a summary; you view them, you do not author them.

Parameters

  • :slugstring (your primaryHost)Required

    Your reseller's domain

  • :tenantIdstring (32-char hex)Required

    Private label tenant id

Response 200on success

{
  status: "OK"
  data: {
    templates: { id: string, title: string, slug: string, isPrivate: boolean, createdAt: number }[]
  }
}

Errors

  • 401
    { "status": "Err", "error": { "code": "UNAUTHORIZED", "message": "…" } }

    Missing, expired or revoked access token — exchange your API key again

  • 403
    { "status": "Err", "error": { "code": "INSUFFICIENT_SCOPE", "message": "…" } }

    Your key does not hold the scope above

  • 403
    { "status": "Err", "error": { "code": "FORBIDDEN", "message": "…" } }

    The slug is not your key's reseller, or the target is outside your subtree

  • 429
    { "error": "Too many requests, please try again later." }

    More than 60 calls a minute from your address — wait for the Retry-After header

People on a private label

Add someone to a private label, list its members, remove someone, or send a fresh invite.

POSTAdd a member to a private label/console/:slug/reseller/private-labels/:tenantId/usersreseller:provision

Adds someone to a private label, creating their account if needed and emailing them a link to set a password. created is false when an account with that email was already on the label.

Parameters

  • :slugstring (your primaryHost)Required

    Your reseller's domain

  • :tenantIdstring (32-char hex)Required

    Private label tenant id

  • emailstringRequired

    Email

  • firstNamestringRequired

    First name

  • lastNamestringRequired

    Last name

Response 200on success

{
  status: "OK"
  data: {
    status: "created" | "added"
    userID: string
    created: boolean
  }
}

Errors

  • 400
    { "status": "Err", "error": { "code": "INVALID_TARGET", "message": "…" } }

    The tenant id is not a live private label

  • 400
    { "status": "Err", "error": { "code": "SEAT_QUOTA_REACHED", "message": "…" } }

    The label has used every seat its package grants

  • 400
    { "status": "Err", "error": { "code": "INVALID_EMAIL", "message": "…" } }

    Not one plain email address — refused before any account exists

  • 400
    { "status": "Err", "error": { "code": "INVALID_NAME", "message": "…" } }

    A blank first or last name

  • 400
    { "status": "Err", "error": { "code": "INVALID_REQUEST", "message": "…" } }

    A body that does not decode — a missing or mistyped field

  • 401
    { "status": "Err", "error": { "code": "UNAUTHORIZED", "message": "…" } }

    Missing, expired or revoked access token — exchange your API key again

  • 403
    { "status": "Err", "error": { "code": "INSUFFICIENT_SCOPE", "message": "…" } }

    Your key does not hold the scope above

  • 403
    { "status": "Err", "error": { "code": "FORBIDDEN", "message": "…" } }

    The slug is not your key's reseller, or the target is outside your subtree

  • 429
    { "error": "Too many requests, please try again later." }

    More than 60 calls a minute from your address — wait for the Retry-After header

GETA private label's members/console/:slug/reseller/private-labels/:tenantId/membersreseller:read

Everyone holding a seat on a private label, owner first. role tells the owner from the label's other admins and its members; isOwner is true for every admin, as it always has been. People who were only sent a document take no seat and are left out.

Parameters

  • :slugstring (your primaryHost)Required

    Your reseller's domain

  • :tenantIdstring (32-char hex)Required

    Private label tenant id

Response 200on success

{
  status: "OK"
  data: {
    members: {
      id: string
      firstName: string
      lastName: string
      email: string
      status: string
      role: "owner" | "admin" | "member"
      isOwner: boolean
      createdAt: number
    }[]
  }
}

Errors

  • 401
    { "status": "Err", "error": { "code": "UNAUTHORIZED", "message": "…" } }

    Missing, expired or revoked access token — exchange your API key again

  • 403
    { "status": "Err", "error": { "code": "INSUFFICIENT_SCOPE", "message": "…" } }

    Your key does not hold the scope above

  • 403
    { "status": "Err", "error": { "code": "FORBIDDEN", "message": "…" } }

    The slug is not your key's reseller, or the target is outside your subtree

  • 429
    { "error": "Too many requests, please try again later." }

    More than 60 calls a minute from your address — wait for the Retry-After header

DELETERemove someone from a private label/console/:slug/reseller/private-labels/:tenantId/users/:userIdreseller:provision

Takes someone off a private label: their seat is revoked, they are signed out at once, and sign-in refuses them. Removing someone already removed answers the same, so a retry is safe. The owner and the label's other admins cannot be removed this way.

Parameters

  • :slugstring (your primaryHost)Required

    Your reseller's domain

  • :tenantIdstring (32-char hex)Required

    Private label tenant id

  • :userIdstring (32-char hex)Required

    User id

Response 200on success

{
  status: "OK"
  data: {
    status: "removed"
  }
}

Errors

  • 400
    { "status": "Err", "error": { "code": "CANNOT_REMOVE_OWNER", "message": "…" } }

    That user is the label's owner or another of its admins

  • 400
    { "status": "Err", "error": { "code": "USER_NOT_FOUND", "message": "…" } }

    No one on this private label has that id

  • 400
    { "status": "Err", "error": { "code": "INVALID_TARGET", "message": "…" } }

    The tenant id is not a live private label

  • 401
    { "status": "Err", "error": { "code": "UNAUTHORIZED", "message": "…" } }

    Missing, expired or revoked access token — exchange your API key again

  • 403
    { "status": "Err", "error": { "code": "INSUFFICIENT_SCOPE", "message": "…" } }

    Your key does not hold the scope above

  • 403
    { "status": "Err", "error": { "code": "FORBIDDEN", "message": "…" } }

    The slug is not your key's reseller, or the target is outside your subtree

  • 429
    { "error": "Too many requests, please try again later." }

    More than 60 calls a minute from your address — wait for the Retry-After header

POSTResend someone's invite/console/:slug/reseller/private-labels/:tenantId/users/:userId/invitereseller:provision

Emails someone on a private label a fresh link to set their password, in the label's branding. The one provisioning sent lasts a day, so a customer whose DNS was not live by then needs another. At most one every 5 minutes per person.

Parameters

  • :slugstring (your primaryHost)Required

    Your reseller's domain

  • :tenantIdstring (32-char hex)Required

    Private label tenant id

  • :userIdstring (32-char hex)Required

    User id

Response 200on success

{
  status: "OK"
  data: {
    status: "sent"
  }
}

Errors

  • 429
    { "status": "Err", "error": { "code": "TOO_FREQUENT", "message": "…" } }

    They were sent one in the last 5 minutes — wait for the Retry-After header

  • 400
    { "status": "Err", "error": { "code": "USER_NOT_FOUND", "message": "…" } }

    No one on this private label has that id

  • 400
    { "status": "Err", "error": { "code": "INVALID_TARGET", "message": "…" } }

    The tenant id is not a live private label

  • 401
    { "status": "Err", "error": { "code": "UNAUTHORIZED", "message": "…" } }

    Missing, expired or revoked access token — exchange your API key again

  • 403
    { "status": "Err", "error": { "code": "INSUFFICIENT_SCOPE", "message": "…" } }

    Your key does not hold the scope above

  • 403
    { "status": "Err", "error": { "code": "FORBIDDEN", "message": "…" } }

    The slug is not your key's reseller, or the target is outside your subtree

  • 429
    { "error": "Too many requests, please try again later." }

    More than 60 calls a minute from your address — wait for the Retry-After header

API keys

See the keys you hold. Minting and revoking keys needs a person signed in to the console.

GETList your API keys/console/:slug/reseller/keysreseller:read

Your API keys, newest first — metadata only, never the secret.

Parameters

  • :slugstring (your primaryHost)Required

    Your reseller's domain

Response 200on success

{
  status: "OK"
  data: {
    keys: { id: string, lastFour: string, scopes: string[], createdAt: number, lastUsedAt: number | null, revokedAt: number | null }[]
  }
}

Errors

  • 401
    { "status": "Err", "error": { "code": "UNAUTHORIZED", "message": "…" } }

    Missing, expired or revoked access token — exchange your API key again

  • 403
    { "status": "Err", "error": { "code": "INSUFFICIENT_SCOPE", "message": "…" } }

    Your key does not hold the scope above

  • 403
    { "status": "Err", "error": { "code": "FORBIDDEN", "message": "…" } }

    The slug is not your key's reseller, or the target is outside your subtree

  • 429
    { "error": "Too many requests, please try again later." }

    More than 60 calls a minute from your address — wait for the Retry-After header

Your packages

The packages you sell to your private labels, with your own codes, prices, currency and billing cycle, and what each one includes.

POSTSearch your packages/console/:slug/reseller/billing/packages/searchreseller:read

The packages you sell to your private labels, a page at a time. sign.net neither sees nor charges these prices.

Parameters

  • :slugstring (your primaryHost)Required

    Your reseller's domain

  • querystringOptional

    Search (empty for all)

  • pagenumber (from 1)Required

    Page

  • pageSizenumber (1 to 100)Required

    Page size

Response 200on success

{
  status: "OK"
  data: {
    packages: {
      id: string
      code: string
      name: string
      currency: string
      billingCycle: "Monthly" | "Quarterly" | "Biannual" | "Annual" | "OneTime"
      basePriceMinor: number
      isActive: boolean
      createdAt: number
      updatedAt: number
    }[]
    total: number
    page: number
    pageSize: number
  }
}

Errors

  • 400
    { "status": "Err", "error": { "code": "INVALID_REQUEST", "message": "…" } }

    A body that does not decode — a missing or mistyped field

  • 401
    { "status": "Err", "error": { "code": "UNAUTHORIZED", "message": "…" } }

    Missing, expired or revoked access token — exchange your API key again

  • 403
    { "status": "Err", "error": { "code": "INSUFFICIENT_SCOPE", "message": "…" } }

    Your key does not hold the scope above

  • 403
    { "status": "Err", "error": { "code": "FORBIDDEN", "message": "…" } }

    The slug is not your key's reseller, or the target is outside your subtree

  • 429
    { "error": "Too many requests, please try again later." }

    More than 60 calls a minute from your address — wait for the Retry-After header

POSTGet one of your packages/console/:slug/reseller/billing/packages/getreseller:read

One package with its items and description. assignedCount is how many live private labels hold it, which is what a price change would reach.

Parameters

  • :slugstring (your primaryHost)Required

    Your reseller's domain

  • idstring (32-char hex)Required

    Package id

Response 200on success

{
  status: "OK"
  data: {
    billingPackage: { id, code, name, description: string | null, currency, billingCycle, basePriceMinor, isActive, createdAt, updatedAt }
    items: { itemCode: "documents" | "seats" | "templates" | "notarizations", includedQty: number, overageBundleSize: number, overageBundlePriceMinor: number }[]
    assignedCount: number
  }
}

Errors

  • 400
    { "status": "Err", "error": { "code": "PACKAGE_NOT_FOUND", "message": "…" } }

    No package of yours has that id — another reseller's reads the same as a missing one

  • 400
    { "status": "Err", "error": { "code": "INVALID_REQUEST", "message": "…" } }

    A body that does not decode — a missing or mistyped field

  • 401
    { "status": "Err", "error": { "code": "UNAUTHORIZED", "message": "…" } }

    Missing, expired or revoked access token — exchange your API key again

  • 403
    { "status": "Err", "error": { "code": "INSUFFICIENT_SCOPE", "message": "…" } }

    Your key does not hold the scope above

  • 403
    { "status": "Err", "error": { "code": "FORBIDDEN", "message": "…" } }

    The slug is not your key's reseller, or the target is outside your subtree

  • 429
    { "error": "Too many requests, please try again later." }

    More than 60 calls a minute from your address — wait for the Retry-After header

POSTCreate a package/console/:slug/reseller/billing/packagesreseller:packages

Adds a package to your catalogue. basePriceMinor is your price to your customer, in minor units; what the package costs you is the quantities it commits, which come off your sign.net allowance when it is assigned.

Parameters

  • :slugstring (your primaryHost)Required

    Your reseller's domain

  • codestringRequired

    Code

  • namestringRequired

    Name

  • descriptionstring | nullRequired

    Description

  • currencystringRequired

    Currency

  • billingCycle"Monthly" | "Quarterly" | "Biannual" | "Annual" | "OneTime"Required

    Billing cycle

  • basePriceMinornumberRequired

    Your package price (minor units)

  • items{ itemCode: "documents" | "seats" | "templates" | "notarizations", includedQty: number, overageBundleSize: number, overageBundlePriceMinor: number }[]Required

    Items

Response 200on success

{
  status: "OK"
  data: {
    id: string
  }
}

Errors

  • 400
    { "status": "Err", "error": { "code": "EMPTY_CODE", "message": "…" } }

    Blank code

  • 400
    { "status": "Err", "error": { "code": "EMPTY_NAME", "message": "…" } }

    Blank name

  • 400
    { "status": "Err", "error": { "code": "DUPLICATE_ITEM", "message": "…" } }

    The same itemCode twice

  • 400
    { "status": "Err", "error": { "code": "INVALID_REQUEST", "message": "…" } }

    Malformed body, an unknown itemCode, a quantity outside 0–10,000,000, or more than four items

  • 400
    { "status": "Err", "error": { "code": "CODE_IN_USE", "message": "…" } }

    You already have one with that code — codes are unique to you, not globally

  • 400
    { "status": "Err", "error": { "code": "CATALOGUE_FULL", "message": "…" } }

    You already hold 100 — deactivating one frees its code, not its slot

  • 401
    { "status": "Err", "error": { "code": "UNAUTHORIZED", "message": "…" } }

    Missing, expired or revoked access token — exchange your API key again

  • 403
    { "status": "Err", "error": { "code": "INSUFFICIENT_SCOPE", "message": "…" } }

    Your key does not hold the scope above

  • 403
    { "status": "Err", "error": { "code": "FORBIDDEN", "message": "…" } }

    The slug is not your key's reseller, or the target is outside your subtree

  • 429
    { "error": "Too many requests, please try again later." }

    More than 60 calls a minute from your address — wait for the Retry-After header

POSTUpdate a package/console/:slug/reseller/billing/packages/updatereseller:packages

Changes a package; only what you send changes. Code, currency and billing cycle are fixed once created. A new price reaches its holders from their next period.

Parameters

  • :slugstring (your primaryHost)Required

    Your reseller's domain

  • idstring (32-char hex)Required

    Package id

  • namestringOptional

    Name

  • descriptionstring | nullOptional

    Description

  • isActivebooleanOptional

    Active

  • basePriceMinornumberOptional

    Your package price (minor units)

  • items{ itemCode: "documents" | "seats" | "templates" | "notarizations", includedQty: number, overageBundleSize: number, overageBundlePriceMinor: number }[]Optional

    Items

Response 200on success

{
  status: "OK"
  data: {
    message: "Success"
  }
}

Errors

  • 400
    { "status": "Err", "error": { "code": "PACKAGE_NOT_FOUND", "message": "…" } }

    No package of yours has that id — another reseller's reads the same as a missing one

  • 400
    { "status": "Err", "error": { "code": "EMPTY_NAME", "message": "…" } }

    Blank name

  • 400
    { "status": "Err", "error": { "code": "DUPLICATE_ITEM", "message": "…" } }

    The same itemCode twice

  • 400
    { "status": "Err", "error": { "code": "INVALID_REQUEST", "message": "…" } }

    Malformed body, an unknown itemCode, a quantity outside 0–10,000,000, or more than four items

  • 401
    { "status": "Err", "error": { "code": "UNAUTHORIZED", "message": "…" } }

    Missing, expired or revoked access token — exchange your API key again

  • 403
    { "status": "Err", "error": { "code": "INSUFFICIENT_SCOPE", "message": "…" } }

    Your key does not hold the scope above

  • 403
    { "status": "Err", "error": { "code": "FORBIDDEN", "message": "…" } }

    The slug is not your key's reseller, or the target is outside your subtree

  • 429
    { "error": "Too many requests, please try again later." }

    More than 60 calls a minute from your address — wait for the Retry-After header

Your add-ons

Extras you can attach on top of a package, each granting more of an item such as seats.

POSTSearch your add-ons/console/:slug/reseller/billing/addons/searchreseller:read

The add-ons you sell on top of your packages, a page at a time.

Parameters

  • :slugstring (your primaryHost)Required

    Your reseller's domain

  • querystringOptional

    Search (empty for all)

  • pagenumber (from 1)Required

    Page

  • pageSizenumber (1 to 100)Required

    Page size

Response 200on success

{
  status: "OK"
  data: {
    addons: {
      id: string
      code: string
      name: string
      currency: string
      priceMinor: number
      billingCycle: "Monthly" | "Quarterly" | "Biannual" | "Annual" | "OneTime"
      isActive: boolean
      createdAt: number
      updatedAt: number
    }[]
    total: number
    page: number
    pageSize: number
  }
}

Errors

  • 400
    { "status": "Err", "error": { "code": "INVALID_REQUEST", "message": "…" } }

    A body that does not decode — a missing or mistyped field

  • 401
    { "status": "Err", "error": { "code": "UNAUTHORIZED", "message": "…" } }

    Missing, expired or revoked access token — exchange your API key again

  • 403
    { "status": "Err", "error": { "code": "INSUFFICIENT_SCOPE", "message": "…" } }

    Your key does not hold the scope above

  • 403
    { "status": "Err", "error": { "code": "FORBIDDEN", "message": "…" } }

    The slug is not your key's reseller, or the target is outside your subtree

  • 429
    { "error": "Too many requests, please try again later." }

    More than 60 calls a minute from your address — wait for the Retry-After header

POSTGet one of your add-ons/console/:slug/reseller/billing/addons/getreseller:read

One add-on with what it grants. areGrantsLocked turns true the first time it is attached anywhere; its price can still change after that, its grants cannot.

Parameters

  • :slugstring (your primaryHost)Required

    Your reseller's domain

  • idstring (32-char hex)Required

    Add-on id

Response 200on success

{
  status: "OK"
  data: {
    addon: { id, code, name, description: string | null, currency, priceMinor, billingCycle, isActive, createdAt, updatedAt }
    items: { itemCode: "documents" | "seats" | "templates" | "notarizations", grantedQty: number }[]
    areGrantsLocked: boolean
    activeAttachments: number
  }
}

Errors

  • 400
    { "status": "Err", "error": { "code": "ADDON_NOT_FOUND", "message": "…" } }

    No add-on of yours has that id — another reseller's reads the same as a missing one

  • 400
    { "status": "Err", "error": { "code": "INVALID_REQUEST", "message": "…" } }

    A body that does not decode — a missing or mistyped field

  • 401
    { "status": "Err", "error": { "code": "UNAUTHORIZED", "message": "…" } }

    Missing, expired or revoked access token — exchange your API key again

  • 403
    { "status": "Err", "error": { "code": "INSUFFICIENT_SCOPE", "message": "…" } }

    Your key does not hold the scope above

  • 403
    { "status": "Err", "error": { "code": "FORBIDDEN", "message": "…" } }

    The slug is not your key's reseller, or the target is outside your subtree

  • 429
    { "error": "Too many requests, please try again later." }

    More than 60 calls a minute from your address — wait for the Retry-After header

POSTCreate an add-on/console/:slug/reseller/billing/addonsreseller:packages

Adds an add-on to your catalogue. It must grant at least one item.

Parameters

  • :slugstring (your primaryHost)Required

    Your reseller's domain

  • codestringRequired

    Code

  • namestringRequired

    Name

  • descriptionstring | nullRequired

    Description

  • currencystringRequired

    Currency

  • billingCycle"Monthly" | "Quarterly" | "Biannual" | "Annual" | "OneTime"Required

    Billing cycle

  • priceMinornumberRequired

    Your add-on price (minor units)

  • items{ itemCode: "documents" | "seats" | "templates" | "notarizations", grantedQty: number }[]Required

    Grants

Response 200on success

{
  status: "OK"
  data: {
    id: string
  }
}

Errors

  • 400
    { "status": "Err", "error": { "code": "NO_GRANTS", "message": "…" } }

    An empty items list — an add-on must grant something

  • 400
    { "status": "Err", "error": { "code": "EMPTY_CODE", "message": "…" } }

    Blank code

  • 400
    { "status": "Err", "error": { "code": "EMPTY_NAME", "message": "…" } }

    Blank name

  • 400
    { "status": "Err", "error": { "code": "DUPLICATE_ITEM", "message": "…" } }

    The same itemCode twice

  • 400
    { "status": "Err", "error": { "code": "INVALID_REQUEST", "message": "…" } }

    Malformed body, an unknown itemCode, a quantity outside 0–10,000,000, or more than four items

  • 400
    { "status": "Err", "error": { "code": "CODE_IN_USE", "message": "…" } }

    You already have one with that code — codes are unique to you, not globally

  • 400
    { "status": "Err", "error": { "code": "CATALOGUE_FULL", "message": "…" } }

    You already hold 100 — deactivating one frees its code, not its slot

  • 401
    { "status": "Err", "error": { "code": "UNAUTHORIZED", "message": "…" } }

    Missing, expired or revoked access token — exchange your API key again

  • 403
    { "status": "Err", "error": { "code": "INSUFFICIENT_SCOPE", "message": "…" } }

    Your key does not hold the scope above

  • 403
    { "status": "Err", "error": { "code": "FORBIDDEN", "message": "…" } }

    The slug is not your key's reseller, or the target is outside your subtree

  • 429
    { "error": "Too many requests, please try again later." }

    More than 60 calls a minute from your address — wait for the Retry-After header

POSTUpdate an add-on/console/:slug/reseller/billing/addons/updatereseller:packages

Changes an add-on; only what you send changes. What it grants can change only until it has been attached somewhere; its price can always change.

Parameters

  • :slugstring (your primaryHost)Required

    Your reseller's domain

  • idstring (32-char hex)Required

    Add-on id

  • namestringOptional

    Name

  • descriptionstring | nullOptional

    Description

  • isActivebooleanOptional

    Active

  • priceMinornumberOptional

    Your add-on price (minor units)

  • items{ itemCode: "documents" | "seats" | "templates" | "notarizations", grantedQty: number }[]Optional

    Grants

Response 200on success

{
  status: "OK"
  data: {
    message: "Success"
  }
}

Errors

  • 400
    { "status": "Err", "error": { "code": "ADDON_NOT_FOUND", "message": "…" } }

    No add-on of yours has that id — another reseller's reads the same as a missing one

  • 400
    { "status": "Err", "error": { "code": "ADDON_IN_USE", "message": "…" } }

    items sent for an add-on that has ever been attached — create a new one instead

  • 400
    { "status": "Err", "error": { "code": "NO_GRANTS", "message": "…" } }

    An empty items list

  • 400
    { "status": "Err", "error": { "code": "EMPTY_NAME", "message": "…" } }

    Blank name

  • 400
    { "status": "Err", "error": { "code": "DUPLICATE_ITEM", "message": "…" } }

    The same itemCode twice

  • 400
    { "status": "Err", "error": { "code": "INVALID_REQUEST", "message": "…" } }

    Malformed body, an unknown itemCode, a quantity outside 0–10,000,000, or more than four items

  • 401
    { "status": "Err", "error": { "code": "UNAUTHORIZED", "message": "…" } }

    Missing, expired or revoked access token — exchange your API key again

  • 403
    { "status": "Err", "error": { "code": "INSUFFICIENT_SCOPE", "message": "…" } }

    Your key does not hold the scope above

  • 403
    { "status": "Err", "error": { "code": "FORBIDDEN", "message": "…" } }

    The slug is not your key's reseller, or the target is outside your subtree

  • 429
    { "error": "Too many requests, please try again later." }

    More than 60 calls a minute from your address — wait for the Retry-After header

Assigning packages

Give a private label one of your packages, attach add-ons to it, and take either off again. Assigning spends your allowance straight away, and going over it takes a second, confirmed request.

GETSee a private label's package/console/:slug/reseller/private-labels/:tenantId/packagereseller:read

The package and add-ons a private label holds, and how much of your allowance they take. assignment is null when it has none.

Parameters

  • :slugstring (your primaryHost)Required

    Your reseller's domain

  • :tenantIdstring (32-char hex)Required

    Private label tenant id

Response 200on success

{
  status: "OK"
  data: {
    assignment: {
      id: string
      packageId: string
      packageCode: string
      packageName: string
      currency: string
      assignedAt: number
      addons: { subscriptionAddonId: string, addonId: string, code: string, name: string, quantity: number }[]
      allocated: { documents: number, seats: number, templates: number, notarizations: number }
    } | null
  }
}

Errors

  • 400
    { "status": "Err", "error": { "code": "INVALID_TARGET", "message": "…" } }

    The tenant id is not a live private label

  • 401
    { "status": "Err", "error": { "code": "UNAUTHORIZED", "message": "…" } }

    Missing, expired or revoked access token — exchange your API key again

  • 403
    { "status": "Err", "error": { "code": "INSUFFICIENT_SCOPE", "message": "…" } }

    Your key does not hold the scope above

  • 403
    { "status": "Err", "error": { "code": "FORBIDDEN", "message": "…" } }

    The slug is not your key's reseller, or the target is outside your subtree

  • 429
    { "error": "Too many requests, please try again later." }

    More than 60 calls a minute from your address — wait for the Retry-After header

POSTGive a private label a package/console/:slug/reseller/private-labels/:tenantId/packagereseller:packages

Gives a private label one of your packages. This spends your sign.net allowance immediately, whether or not the label uses it, and caps the label at what it was given. Going past your allowance takes a second call with the confirmation key.

Parameters

  • :slugstring (your primaryHost)Required

    Your reseller's domain

  • :tenantIdstring (32-char hex)Required

    Private label tenant id

  • packageIdstringRequired

    Package

  • confirmationKeystringOptional

    Confirmation key (only to go over your allowance)

Response 200on success

{
  status: "OK"
  data: {
    outcome: "Assigned"
    assignmentId: string
    allocated: { documents: number, seats: number, templates: number, notarizations: number }
  }
}

Errors

  • 200
    { "status": "OK", "data": { "outcome": "ConfirmationRequired", "confirmation": { "key": "…", "expiresAt": 1758800000000 }, "warnings": [{ "itemCode": "seats", "requested": 10, "remaining": 3, "excess": 7 }] } }

    It would take you past your allowance. NOTHING was written — send the identical body again with confirmationKey to go ahead

  • 400
    { "status": "Err", "error": { "code": "ALREADY_ASSIGNED", "message": "…" } }

    That label already has a package — remove it first, or attach an add-on

  • 400
    { "status": "Err", "error": { "code": "PACKAGE_NOT_FOUND", "message": "…" } }

    No package of yours has that id — another reseller's reads the same as a missing one

  • 400
    { "status": "Err", "error": { "code": "PACKAGE_INACTIVE", "message": "…" } }

    That package is deactivated

  • 400
    { "status": "Err", "error": { "code": "NO_SUBSCRIPTION", "message": "…" } }

    sign.net has not put you on a plan, so there is no allowance to assign from

  • 400
    { "status": "Err", "error": { "code": "INVALID_TARGET", "message": "…" } }

    The tenant id is not a live private label

  • 400
    { "status": "Err", "error": { "code": "CONFIRMATION_INVALID", "message": "…" } }

    The confirmationKey was issued for a different request

  • 400
    { "status": "Err", "error": { "code": "CONFIRMATION_EXPIRED", "message": "…" } }

    The confirmationKey is over 10 minutes old — send again without it for a new one

  • 401
    { "status": "Err", "error": { "code": "UNAUTHORIZED", "message": "…" } }

    Missing, expired or revoked access token — exchange your API key again

  • 403
    { "status": "Err", "error": { "code": "INSUFFICIENT_SCOPE", "message": "…" } }

    Your key does not hold the scope above

  • 403
    { "status": "Err", "error": { "code": "FORBIDDEN", "message": "…" } }

    The slug is not your key's reseller, or the target is outside your subtree

  • 429
    { "error": "Too many requests, please try again later." }

    More than 60 calls a minute from your address — wait for the Retry-After header

DELETERemove a private label's package/console/:slug/reseller/private-labels/:tenantId/packagereseller:packages

Takes a private label's package back and frees the allowance for future promises. The period it was held in is still billed for it.

Parameters

  • :slugstring (your primaryHost)Required

    Your reseller's domain

  • :tenantIdstring (32-char hex)Required

    Private label tenant id

Response 200on success

{
  status: "OK"
  data: {
    outcome: "Unassigned"
  }
}

Errors

  • 400
    { "status": "Err", "error": { "code": "NOT_ASSIGNED", "message": "…" } }

    That private label has no package of yours — none at all, or one another reseller assigned before it moved to you

  • 400
    { "status": "Err", "error": { "code": "INVALID_TARGET", "message": "…" } }

    The tenant id is not a live private label

  • 401
    { "status": "Err", "error": { "code": "UNAUTHORIZED", "message": "…" } }

    Missing, expired or revoked access token — exchange your API key again

  • 403
    { "status": "Err", "error": { "code": "INSUFFICIENT_SCOPE", "message": "…" } }

    Your key does not hold the scope above

  • 403
    { "status": "Err", "error": { "code": "FORBIDDEN", "message": "…" } }

    The slug is not your key's reseller, or the target is outside your subtree

  • 429
    { "error": "Too many requests, please try again later." }

    More than 60 calls a minute from your address — wait for the Retry-After header

POSTAttach an add-on to a private label/console/:slug/reseller/private-labels/:tenantId/package/addonsreseller:packages

Tops a private label up without changing its package. Spends granted quantity × quantity of your allowance, the same way assigning does.

Parameters

  • :slugstring (your primaryHost)Required

    Your reseller's domain

  • :tenantIdstring (32-char hex)Required

    Private label tenant id

  • addonIdstringRequired

    Add-on

  • quantitynumber (at least 1)Required

    Quantity

  • confirmationKeystringOptional

    Confirmation key (only to go over your allowance)

Response 200on success

{
  status: "OK"
  data: {
    outcome: "Attached"
    subscriptionAddonId: string
  }
}

Errors

  • 200
    { "status": "OK", "data": { "outcome": "ConfirmationRequired", "confirmation": { "key": "…", "expiresAt": 1758800000000 }, "warnings": [{ "itemCode": "seats", "requested": 10, "remaining": 3, "excess": 7 }] } }

    It would take you past your allowance. NOTHING was written — send the identical body again with confirmationKey to go ahead

  • 400
    { "status": "Err", "error": { "code": "ALREADY_ATTACHED", "message": "…" } }

    That add-on is already on this label — remove it and attach it again to change the quantity

  • 400
    { "status": "Err", "error": { "code": "NOT_ASSIGNED", "message": "…" } }

    That label has no package of yours — assign one before adding to it

  • 400
    { "status": "Err", "error": { "code": "ADDON_NOT_FOUND", "message": "…" } }

    No add-on of yours has that id — another reseller's reads the same as a missing one

  • 400
    { "status": "Err", "error": { "code": "ADDON_INACTIVE", "message": "…" } }

    That add-on is deactivated

  • 400
    { "status": "Err", "error": { "code": "INVALID_QUANTITY", "message": "…" } }

    Quantity below 1 (a zero is a mistake, not a no-op), or one granting more than 10,000,000 of an item

  • 400
    { "status": "Err", "error": { "code": "NO_SUBSCRIPTION", "message": "…" } }

    sign.net has not put you on a plan, so there is no allowance to assign from

  • 400
    { "status": "Err", "error": { "code": "INVALID_TARGET", "message": "…" } }

    The tenant id is not a live private label

  • 400
    { "status": "Err", "error": { "code": "CONFIRMATION_INVALID", "message": "…" } }

    The confirmationKey was issued for a different request

  • 400
    { "status": "Err", "error": { "code": "CONFIRMATION_EXPIRED", "message": "…" } }

    The confirmationKey is over 10 minutes old — send again without it for a new one

  • 401
    { "status": "Err", "error": { "code": "UNAUTHORIZED", "message": "…" } }

    Missing, expired or revoked access token — exchange your API key again

  • 403
    { "status": "Err", "error": { "code": "INSUFFICIENT_SCOPE", "message": "…" } }

    Your key does not hold the scope above

  • 403
    { "status": "Err", "error": { "code": "FORBIDDEN", "message": "…" } }

    The slug is not your key's reseller, or the target is outside your subtree

  • 429
    { "error": "Too many requests, please try again later." }

    More than 60 calls a minute from your address — wait for the Retry-After header

DELETERemove an add-on from a private label/console/:slug/reseller/private-labels/:tenantId/package/addons/:subscriptionAddonIdreseller:packages

Takes an add-on back off a private label, releasing what it allocated.

Parameters

  • :slugstring (your primaryHost)Required

    Your reseller's domain

  • :tenantIdstring (32-char hex)Required

    Private label tenant id

  • :subscriptionAddonIdstring (32-char hex)Required

    Attached add-on id

Response 200on success

{
  status: "OK"
  data: {
    outcome: "Removed"
  }
}

Errors

  • 400
    { "status": "Err", "error": { "code": "NOT_ASSIGNED", "message": "…" } }

    That private label has no package of yours — none at all, or one another reseller assigned before it moved to you

  • 400
    { "status": "Err", "error": { "code": "ADDON_NOT_FOUND", "message": "…" } }

    Nothing attached to this label has that subscriptionAddonId

  • 400
    { "status": "Err", "error": { "code": "INVALID_TARGET", "message": "…" } }

    The tenant id is not a live private label

  • 401
    { "status": "Err", "error": { "code": "UNAUTHORIZED", "message": "…" } }

    Missing, expired or revoked access token — exchange your API key again

  • 403
    { "status": "Err", "error": { "code": "INSUFFICIENT_SCOPE", "message": "…" } }

    Your key does not hold the scope above

  • 403
    { "status": "Err", "error": { "code": "FORBIDDEN", "message": "…" } }

    The slug is not your key's reseller, or the target is outside your subtree

  • 429
    { "error": "Too many requests, please try again later." }

    More than 60 calls a minute from your address — wait for the Retry-After header

Usage, quota and points

What your private labels used in a month, your allowance from Sign.net against what you have allocated, and your points pools and their ledgers.

GETUsage roll-up/console/:slug/reseller/usagereseller:read

What your private labels used in a month — documents, notarisations, points, seats and templates — in total and per label. Leave the period out for the current month.

Parameters

  • :slugstring (your primaryHost)Required

    Your reseller's domain

  • ?periodstring (YYYY-MM)Optional

    Period

Response 200on success

{
  status: "OK"
  data: {
    period: string
    total: { documents: number, notarizations: number, points: number, seats: number, templates: number }
    privateLabels: {
      tenantId: string
      primaryHost: string
      metrics: { documents: number, notarizations: number, points: number, seats: number, templates: number }
    }[]
  }
}

Errors

  • 400
    { "status": "Err", "error": { "code": "INVALID_PERIOD", "message": "…" } }

    A period that is not YYYY-MM

  • 401
    { "status": "Err", "error": { "code": "UNAUTHORIZED", "message": "…" } }

    Missing, expired or revoked access token — exchange your API key again

  • 403
    { "status": "Err", "error": { "code": "INSUFFICIENT_SCOPE", "message": "…" } }

    Your key does not hold the scope above

  • 403
    { "status": "Err", "error": { "code": "FORBIDDEN", "message": "…" } }

    The slug is not your key's reseller, or the target is outside your subtree

  • 429
    { "error": "Too many requests, please try again later." }

    More than 60 calls a minute from your address — wait for the Retry-After header

GETYour allowance, and what you have allocated/console/:slug/reseller/billing/quotareseller:read

Your plan with sign.net, what you have promised out to your labels, and what they actually used — counts, never sign.net's prices. remaining is what is left once what is allocated to your labels, what your own team used (ownUsed) and what labels used beyond their packages (labelsBeyondAllocation) are taken off: assigning spends a label's share straight away, so its usage inside the package takes nothing more. subscription is null when sign.net has not put you on a plan.

Parameters

  • :slugstring (your primaryHost)Required

    Your reseller's domain

Response 200on success

{
  status: "OK"
  data: {
    subscription: { packageName: string, billingCycle: string } | null
    window: { from: number, to: number } | null
    items: {
      itemCode: "documents" | "seats" | "templates" | "notarizations"
      includedQty: number
      allocatedNow: number
      used: number
      ownUsed: number
      labelsBeyondAllocation: number
      remaining: number
      overAllocatedBy: number
    }[]
    byPrivateLabel: {
      tenantId: string
      primaryHost: string
      allocatedInPeriod: { documents: number, seats: number, templates: number, notarizations: number }
      actual: { documents: number, seats: number, templates: number, notarizations: number }
      billed: { documents: number, seats: number, templates: number, notarizations: number }
    }[]
  }
}

Errors

  • 401
    { "status": "Err", "error": { "code": "UNAUTHORIZED", "message": "…" } }

    Missing, expired or revoked access token — exchange your API key again

  • 403
    { "status": "Err", "error": { "code": "INSUFFICIENT_SCOPE", "message": "…" } }

    Your key does not hold the scope above

  • 403
    { "status": "Err", "error": { "code": "FORBIDDEN", "message": "…" } }

    The slug is not your key's reseller, or the target is outside your subtree

  • 429
    { "error": "Too many requests, please try again later." }

    More than 60 calls a minute from your address — wait for the Retry-After header

GETYour points pool/console/:slug/reseller/billing/pointsreseller:read

Your points pool and each private label's. Units are the truth; points are units × sign.net's rate card. A label with hasPool: false has no package and spends from drawsFrom — you. Counts and points only, never prices.

Parameters

  • :slugstring (your primaryHost)Required

    Your reseller's domain

Response 200on success

{
  status: "OK"
  data: {
    reseller: {
      tenantId: string
      primaryHost: string
      hasPool: boolean
      drawsFrom: string | null
      buckets: { itemCode: "documents" | "seats" | "templates" | "notarizations", units: number, pointsPerUnit: number, points: number, activity: { Grant, Transfer, Return, Consume, Release, Overage, Revoke, Adjust, Expire, Opening: number }, carried: number | null, nextLapse: { units: number, at: number } | null }[]
    }
    cycle: { start: number, end: number | null } | null
    rates: { documents: number, seats: number, templates: number, notarizations: number }
    privateLabels: {
      tenantId: string
      primaryHost: string
      hasPool: boolean
      drawsFrom: string | null
      buckets: { itemCode: "documents" | "seats" | "templates" | "notarizations", units: number, pointsPerUnit: number, points: number, activity: { Grant, Transfer, Return, Consume, Release, Overage, Revoke, Adjust, Expire, Opening: number }, carried: number | null, nextLapse: { units: number, at: number } | null }[]
    }[]
  }
}

Errors

  • 401
    { "status": "Err", "error": { "code": "UNAUTHORIZED", "message": "…" } }

    Missing, expired or revoked access token — exchange your API key again

  • 403
    { "status": "Err", "error": { "code": "INSUFFICIENT_SCOPE", "message": "…" } }

    Your key does not hold the scope above

  • 403
    { "status": "Err", "error": { "code": "FORBIDDEN", "message": "…" } }

    The slug is not your key's reseller, or the target is outside your subtree

  • 429
    { "error": "Too many requests, please try again later." }

    More than 60 calls a minute from your address — wait for the Retry-After header

GETYour points ledger/console/:slug/reseller/billing/points/ledgerreseller:read

Every movement on your own buckets, newest first, a page at a time. itemCode narrows it to one item.

Parameters

  • :slugstring (your primaryHost)Required

    Your reseller's domain

  • ?itemCode"documents" | "seats" | "templates" | "notarizations"Optional

    Item

  • ?pagenumber (from 1)Optional

    Page

  • ?pageSizenumber (1 to 100)Optional

    Page size

Response 200on success

{
  status: "OK"
  data: {
    rows: {
      id: string
      tenantId: string
      itemCode: "documents" | "seats" | "templates" | "notarizations"
      kind: "Grant" | "Transfer" | "Return" | "Consume" | "Release" | "Overage" | "Revoke" | "Adjust" | "Expire" | "Opening"
      units: number
      pointsPerUnit: number
      counterpartTenantId: string | null
      sourceType: string
      sourceId: string | null
      cycleStart: number | null
      expiresAt: number | null
      bundleSize: number | null
      isCapped: boolean | null
      note: string | null
      createdAt: number
    }[]
    total: number
  }
}

Errors

  • 400
    { "status": "Err", "error": { "code": "INVALID_QUERY", "message": "…" } }

    page below 1, pageSize outside 1–100, or an unknown itemCode

  • 401
    { "status": "Err", "error": { "code": "UNAUTHORIZED", "message": "…" } }

    Missing, expired or revoked access token — exchange your API key again

  • 403
    { "status": "Err", "error": { "code": "INSUFFICIENT_SCOPE", "message": "…" } }

    Your key does not hold the scope above

  • 403
    { "status": "Err", "error": { "code": "FORBIDDEN", "message": "…" } }

    The slug is not your key's reseller, or the target is outside your subtree

  • 429
    { "error": "Too many requests, please try again later." }

    More than 60 calls a minute from your address — wait for the Retry-After header

GETA private label's points/console/:slug/reseller/private-labels/:tenantId/pointsreseller:read

One private label's buckets, or — for a label with no package — the pool it spends from.

Parameters

  • :slugstring (your primaryHost)Required

    Your reseller's domain

  • :tenantIdstring (32-char hex)Required

    Private label tenant id

Response 200on success

{
  status: "OK"
  data: {
    pool: {
      tenantId: string
      primaryHost: string
      hasPool: boolean
      drawsFrom: string | null
      buckets: { itemCode: "documents" | "seats" | "templates" | "notarizations", units: number, pointsPerUnit: number, points: number, activity: { Grant, Transfer, Return, Consume, Release, Overage, Revoke, Adjust, Expire, Opening: number }, carried: number | null, nextLapse: { units: number, at: number } | null }[]
    }
    cycle: { start: number, end: number | null } | null
    rates: { documents: number, seats: number, templates: number, notarizations: number }
  }
}

Errors

  • 400
    { "status": "Err", "error": { "code": "INVALID_TARGET", "message": "…" } }

    The tenant id is not a live private label

  • 401
    { "status": "Err", "error": { "code": "UNAUTHORIZED", "message": "…" } }

    Missing, expired or revoked access token — exchange your API key again

  • 403
    { "status": "Err", "error": { "code": "INSUFFICIENT_SCOPE", "message": "…" } }

    Your key does not hold the scope above

  • 403
    { "status": "Err", "error": { "code": "FORBIDDEN", "message": "…" } }

    The slug is not your key's reseller, or the target is outside your subtree

  • 429
    { "error": "Too many requests, please try again later." }

    More than 60 calls a minute from your address — wait for the Retry-After header

GETA private label's points ledger/console/:slug/reseller/private-labels/:tenantId/points/ledgerreseller:read

Every movement on one private label's buckets, newest first, a page at a time.

Parameters

  • :slugstring (your primaryHost)Required

    Your reseller's domain

  • :tenantIdstring (32-char hex)Required

    Private label tenant id

  • ?itemCode"documents" | "seats" | "templates" | "notarizations"Optional

    Item

  • ?pagenumber (from 1)Optional

    Page

  • ?pageSizenumber (1 to 100)Optional

    Page size

Response 200on success

{
  status: "OK"
  data: {
    rows: {
      id: string
      tenantId: string
      itemCode: "documents" | "seats" | "templates" | "notarizations"
      kind: "Grant" | "Transfer" | "Return" | "Consume" | "Release" | "Overage" | "Revoke" | "Adjust" | "Expire" | "Opening"
      units: number
      pointsPerUnit: number
      counterpartTenantId: string | null
      sourceType: string
      sourceId: string | null
      cycleStart: number | null
      expiresAt: number | null
      bundleSize: number | null
      isCapped: boolean | null
      note: string | null
      createdAt: number
    }[]
    total: number
  }
}

Errors

  • 400
    { "status": "Err", "error": { "code": "INVALID_QUERY", "message": "…" } }

    page below 1, pageSize outside 1–100, or an unknown itemCode

  • 400
    { "status": "Err", "error": { "code": "INVALID_TARGET", "message": "…" } }

    The tenant id is not a live private label

  • 401
    { "status": "Err", "error": { "code": "UNAUTHORIZED", "message": "…" } }

    Missing, expired or revoked access token — exchange your API key again

  • 403
    { "status": "Err", "error": { "code": "INSUFFICIENT_SCOPE", "message": "…" } }

    Your key does not hold the scope above

  • 403
    { "status": "Err", "error": { "code": "FORBIDDEN", "message": "…" } }

    The slug is not your key's reseller, or the target is outside your subtree

  • 429
    { "error": "Too many requests, please try again later." }

    More than 60 calls a minute from your address — wait for the Retry-After header

Sell portals from WHMCS

The Sign.net Reseller plugin for WHMCS calls this API for you, so there's no code to write. A paid order creates the portal, and it is suspended, changed and deleted with its service. You manage your packages and add-ons, and see your allowance, inside WHMCS.

How it works

Each WHMCS service is one private-label portal, on the address your customer chose at checkout, holding one of your Sign.net packages. On their service page, your customer sees the portal's state, the DNS records to create, each with a Copy button, and what their package includes.

What each WHMCS action does

Order paid, or Create
Creates the portal on the order's Portal address, with the client as its owner, the product's package and the add-ons they chose. The owner is emailed a link to set a password.
Suspend, Unsuspend
Suspends the portal with WHMCS's reason, which you and Sign.net see and its people never do, or lifts it. Only Sign.net can lift a suspension Sign.net made.
Change Package
Swaps the package and changes add-on quantities to match, for upgrades, downgrades and configurable options.
Terminate
Deletes the portal for good. Its hostname can never be used again.

What you need

  • WHMCS 8.13 LTS on PHP 8.1 to 8.3, or WHMCS 9.0 on PHP 8.2 or later.
  • The PHP extensions curl and json. intl is optional: it converts internationalised portal addresses, such as bücher.example, to their xn-- form.
  • A Sign.net plan on your reseller account. Without one, Sign.net refuses to assign packages.
  • An API key holding all three scopes: reseller:read, reseller:provision and reseller:packages.

Install it

  1. Download signnet-whmcs-plugin-<version>.zip from the latest release and unzip it. Copy its modules/servers/signnet/ and modules/addons/signnet_reseller/ folders into your WHMCS root, keeping their paths. Nothing else is needed: the plugin uses no Composer at runtime.

  2. Activate the addon: System Settings → Addon Modules → Sign.net Reseller → Activate. Then click Configure, give your admin roles access, and set the branding new portals start with and how low an allowance gets before the dashboard warns you.

  3. Add the Sign.net server: System Settings → Servers → Add New Server. Choose the module Sign.net Private Label, enter api-app.sign.net as the hostname and your snk_live_… key as the password, and tick Secure. Click Test Connection, then add the server to a server group.

  4. Create a package: Addons → Sign.net Reseller → Packages → New package, with a code, a name, the billing cycle and price you sell it at, and what it includes. A code can never be reused, even after the package is archived.

  5. Turn it into a product: click WHMCS product on the package's row, choose a product group and click Create product. The product is set up when the order is paid, sends the Sign.net Portal Welcome email, is priced from the package, and asks for a Portal address and Portal name at checkout.

  6. Sell add-ons, if you like: Addons → Sign.net Reseller → Add-ons → New add-on. Click Configurable option on its row, tick the products that offer it and click Create configurable option. Keep the addon_<CODE>| part of the option's name: it's how the plugin finds the add-on.

The product's Module Settings

Sign.net package
Your active packages, read live from Sign.net.
Orders over the allowance
Hold for approval, the default: an order that would take you past your Sign.net allowance waits, and nothing is created until you approve it. Confirm automatically: it goes ahead, and you're billed for the excess.
Automated termination
Only after a cancellation request, the default: the cron deletes a portal only when the client asked to cancel. Always: on any termination, overdue ones included. Never: only an administrator deletes. An administrator's own Terminate always deletes.

Running it

On the service page

  • Approve over-allowance & retry

    Lets an order held over your allowance go ahead. A held order stays Pending, its Create error starts [signnet:held], and it's listed on the addon's dashboard.

  • Resend owner invite

    The owner's link to set a password expires after 24 hours. Sign.net sends at most one every five minutes.

  • Retry domain attach

    The portal's address shows Not attached.

  • Apply plan

    The portal's package or add-ons don't match the service, such as after you change the product's package.

  • Link existing portal

    The portal already exists, such as one you made in the reseller console. Put its tenant id in the service's Username field, or its address in Domain, save, then click.

Limits to know

  • There's no sandbox

    The portals a key creates are real, and so is what they use.

  • A hostname is used once

    One that was ever used, even by a deleted portal or another reseller, can never be used again. Checkout can only check the hostnames your own WHMCS knows about.

  • About 20 new portals a minute

    Per reseller account, shared by all its keys, and fewer when orders carry add-ons. Retry a refused order from WHMCS's module queue: a retry never makes a second portal.

  • Package swaps aren't atomic

    For a moment the portal holds no package, and the credit it carried is written off. If the new package can't be assigned, the old one and its add-ons are put back.

  • Token requests

    Sign.net allows 5 every 15 minutes from one address. The plugin shares one token across WHMCS, and after a refused key it waits five minutes before trying that key again, Test Connection included.

To upgrade, copy a new release's module folders over the old ones: the addon adds any tables and columns it needs and never removes any. The welcome email's merge fields, held orders and what to do when a call fails are all in the plugin's install guide.