Payna is backed by Y Combinator

Getting started

Quickstart

From nothing to a live licensure check. If you already have a key, skip to step three.

1. Mint an API key

In the Payna dashboard, open Settings, then API Keys, and create a key with a label describing the system that will use it. You need to be an admin on the account.

2. Put it in the environment

# Store the key in the environment rather than in source

export PAYNA_API_KEY="pk_live_..."

A key grants read access to your whole compliance portfolio, so treat it like a database credential. It belongs on a server. Never ship one in browser code, a mobile binary, or a public repository.

3. Call the summary endpoint

The summary is the cheapest call in the API and the best way to confirm a key works.

curl -s https://app.payna.com/api/v1/licenses/summary \
  -H "Authorization: Bearer $PAYNA_API_KEY"

You should get back aggregate counts across the portfolio.

{
  "data": {
    "total": 37,
    "by_status": {
      "active": 13,
      "deficient": 2,
      "pending": 3,
      "applying": 10,
      "not_licensed": 9
    },
    "renewing_soon": 3,
    "renewing_30_days": 1,
    "expired": 0,
    "verified": 0
  },
  "meta": { "total": 37, "synced_at": null }
}

A 401 here means the key is wrong, malformed, or revoked. A 403 means the key is genuine but is not entitled to this resource. Both are covered on the errors page.

4. Ask a real question

Aggregates are for dashboards. The call that tends to matter is whether a specific state is good right now.

# The question most integrations actually ask:
# can we operate in this state today?

curl -s https://app.payna.com/api/v1/licenses/CA \
  -H "Authorization: Bearer $PAYNA_API_KEY"

5. Wrap it in a client

A minimal client with the two things every integration needs: rate limit handling and a real error path.

const BASE = 'https://app.payna.com/api/v1'

async function payna(path) {
  const res = await fetch(BASE + path, {
    headers: { Authorization: `Bearer ${process.env.PAYNA_API_KEY}` },
  })

  if (res.status === 429) {
    // Retry-After is in seconds and is always present on a 429.
    const wait = Number(res.headers.get('Retry-After') ?? 30)
    throw new Error(`Rate limited, retry in ${wait}s`)
  }

  const body = await res.json()
  if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`)
  return body
}

// Gate on the derived descriptor, not the raw status string.
const { data } = await payna('/licenses/CA')
const canOperate = data.status_detail.category === 'active'

Next

The interactive reference accepts your key and runs requests against your own data, which is usually faster than reading schemas.

See how Payna runs licensing.

Tell us a little about the company and we’ll walk through how Payna handles licensing and compliance in every US state.