Step 2 of 5

Authentication

Sign a JWT, exchange it for an authentication token in Vanguard, authorize an account to get the authorization token the Invoice API requires, and make a first call with it.

Concepts

Four ideas explain how Waya's authentication is organized:

  • An integration is your software, identified by your Integration ID. One integration can reach several organizations.
  • An organization is a creditor, which these docs also call a client.
  • An organization has one or more accounts, for example one per service it uses at Waya.
  • An account belongs to exactly one organization.

You call the Invoice Tools API on behalf of an account. Vanguard, Waya's authentication service, checks who you are and which accounts you may act on.

The flow

  1. Sign a JWT with your key material.
  2. Send the JWT to Vanguard. It returns an authentication token.
  3. Use the authentication token to authorize an account. Vanguard returns an authorization token.
  4. Call the Invoice API with the authorization token.

Two more calls are optional: finding your account and verifying a token.

Two tokens, two lifetimes

The authentication token identifies your integration and can authorize several accounts. It lasts about a week. The authorization token is tied to an account, and it is the token the Invoice API accepts. It lasts about 24 hours.

Both lifetimes are approximate. Build renewal into your integration instead of counting on exact times, as described under Renewal guidance.

Sign a JWT

Your app proves who it is by signing a JWT with the key material from Waya. Sign with the HS512 algorithm.

Keep the key material secret

The key material signs every token your integration creates. Never commit it to version control, put it in client-side code or share it. Keep your .env file out of version control as well.

These are the parts of the token:

PartNameValue
HeaderalgHS512
HeaderkidYour Key ID
ClaimissYour Issuer URL
ClaimiidYour Integration ID
ClaimiatWhen the JWT was issued, in Unix seconds
ClaimexpWhen the JWT expires, in Unix seconds

The JWT is used once, to get an authentication token, so a short lifetime is enough. The samples set exp to 10 minutes after iat.

The key material is base64url

The key material is encoded as base64url, which differs slightly from the default base64: it uses - and _ instead of + and /, and it can leave out the = padding. Decode it as base64url first, then use the decoded bytes as the HS512 key. A standard base64 decoder rejects the text or gives the wrong key. Signing with the encoded text instead of the decoded bytes uses a different key, so Vanguard cannot verify the signature.

Each sample reads the WAYA_* variables from Get started and prints a JWT. They use these libraries:

LanguageLibraryVersion
Javajjwt0.12 (jjwt-api, with jjwt-impl and jjwt-jackson at runtime)
Node.jsjose5
PythonPyJWT2
C#Microsoft.IdentityModel.JsonWebTokens7 or 8, on .NET 8
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Base64;
import java.util.Date;
import javax.crypto.SecretKey;

public class WayaJwt {

  static String createJwt(
      String integrationId, String issuerUrl, String keyId, String keyMaterial, int minutes) {
    // The key material is base64url, so use the URL decoder, not Base64.getDecoder().
    SecretKey key = Keys.hmacShaKeyFor(Base64.getUrlDecoder().decode(keyMaterial));
    Instant now = Instant.now();

    return Jwts.builder()
        .issuer(issuerUrl)
        .header().keyId(keyId).and()
        .claim("iid", integrationId)
        .issuedAt(Date.from(now))
        .expiration(Date.from(now.plus(minutes, ChronoUnit.MINUTES)))
        // Name HS512 explicitly, so the algorithm never depends on the key length.
        .signWith(key, Jwts.SIG.HS512)
        .compact();
  }

  public static void main(String[] args) {
    System.out.println(createJwt(
        System.getenv("WAYA_INTEGRATION_ID"),
        System.getenv("WAYA_ISSUER_URL"),
        System.getenv("WAYA_KEY_ID"),
        System.getenv("WAYA_KEY_MATERIAL"),
        10));
  }
}

The Java sample uses the jjwt 0.12 builder. On jjwt 0.11, use the calls that Vanguard's Swagger shows instead: setIssuer, setHeaderParam("kid", keyId), setIssuedAt, setExpiration and signWith(key). In 0.12, setIssuer, setHeaderParam, setIssuedAt and setExpiration still work but are deprecated. signWith(key) is not deprecated.

Decoded, the token looks like this. The values of iat and exp are examples:

Header
{
  "alg": "HS512",
  "kid": "<Key ID>"
}
Payload
{
  "iss": "<Issuer URL>",
  "iid": "<Integration ID>",
  "iat": 1761987600,
  "exp": 1761988200
}

Get an authentication token

Send the JWT to Vanguard as subject.

POSTauthenticate as integration with subject token/v2.0/authentications/integrationsauthenticate as integration with subject token in Swagger (opens in a new tab)
Shell
curl --request POST \
  --url 'https://vanguard.sandbox.waya.se/v2.0/authentications/integrations' \
  --header 'Content-Type: application/json' \
  --data '{ "subject": "<jwt>" }'

Vanguard answers 200 with the authentication token. error is null when the call succeeds.

JSON
{
  "token": "<authentication-token>",
  "error": null
}

The authentication token lasts about a week and can authorize several accounts. Keep it and reuse it. Do not sign a new JWT for every call.

Find your account

This step is optional. Waya gives you an Account ID, so you only need this call to list the organizations and accounts that your integration can reach. Use the authentication token.

GETlist organizations/v2.0/organizationslist organizations in Swagger (opens in a new tab)
Shell
curl --request GET \
  --url 'https://vanguard.sandbox.waya.se/v2.0/organizations' \
  --header 'Authorization: Bearer <authentication-token>'

An optional search query parameter filters the list by text.

JSON
{
  "result": {
    "count": 1,
    "totalCount": 1,
    "more": false,
    "organizations": [
      {
        "id": "ORG-123",
        "name": "Example Client AB",
        "accounts": [
          {
            "id": "DEF-456",
            "name": "Example Client AB",
            "user": null,
            "credentials": null
          }
        ]
      }
    ]
  },
  "error": null
}

The account IDs are at result.organizations[].accounts[].id. An organization can have several accounts, for example one per service.

Authorize an account

Ask Vanguard for an authorization token for one account. Send the authentication token in the Authorization header, and the account in the query string.

POSTauthorize against a set of organizations or accounts/v2.0/authorizationsauthorize against a set of organizations or accounts in Swagger (opens in a new tab)
Shell
curl --request POST \
  --url 'https://vanguard.sandbox.waya.se/v2.0/authorizations?accountId=<account-id>' \
  --header 'Authorization: Bearer <authentication-token>' \
  --header 'Content-Length: 0'

Use your Account ID as <account-id>. Vanguard answers 200 with the authorization token:

JSON
{
  "token": "<authorization-token>",
  "error": null
}

The authorization token lasts about 24 hours, so renew it automatically. Send it to the Invoice API in the Authorization header, as Bearer <authorization-token>.

Vanguard also accepts organizationId, but you typically authorize a single account.

Verify a token

This step is optional. It checks an authorization token and reports its scope.

POSTvalidate and report scope/v2.0/authorizations/tokensvalidate and report scope in Swagger (opens in a new tab)

The token to verify goes in the body as subject. The Authorization header holds your authentication token, or another valid authorization token.

Shell
curl --request POST \
  --url 'https://vanguard.sandbox.waya.se/v2.0/authorizations/tokens' \
  --header 'Authorization: Bearer <authentication-token>' \
  --header 'Content-Type: application/json' \
  --data '{ "subject": "<authorization-token>" }'

The token is good when valid is true and description.organizations holds at least one organization. description.expiresAt tells when it expires.

JSON
{
  "valid": true,
  "description": {
    "expiresAt": "2026-11-02T08:56:28.891196Z",
    "maxCacheTimeMs": 5000,
    "apis": [
      "INVOICE_API_3"
    ],
    "organizations": [
      {
        "id": "ORG-123",
        "name": "Example Client AB",
        "accounts": [
          {
            "id": "DEF-456",
            "name": "Example Client AB",
            "user": null,
            "credentials": null
          }
        ]
      }
    ]
  },
  "error": null
}

Renewal guidance

  • Keep both tokens in memory, or in a cache shared by your instances, and reuse them. Do not sign a JWT or call Vanguard for every request.
  • Renew the authorization token before it expires. Note when you got it, and authorize again well inside its 24 hours.
  • When the authentication token expires, start over: sign a new JWT, get a new authentication token, and authorize your account again.
  • As a safety net, treat a 401 from the Invoice API as a sign that the authorization token is no longer valid. Get a new one and retry the request once.

First call

With an authorization token, list your invoices with GET /invoice. The base URL of the Invoice API in the sandbox is https://api.sandbox.waya.se/v4/rest.

Send the authorization token in the Authorization header:

Shell
curl --request GET \
  --url 'https://api.sandbox.waya.se/v4/rest/invoice' \
  --header 'Authorization: Bearer <authorization-token>'

A fresh sandbox has no invoices yet, so the answer is 200 with an empty data array.

The base URL itself is not an endpoint. A GET to it redirects to Swagger instead of returning your invoices, so always include /invoice.

If the call fails with 401 (319-2-5), the token was not accepted. Check that you send the authorization token, not the authentication token, and that it has not expired. All error codes are on the Errors page.

Once the call works, create your first invoice.