Getting Started · Step 1 of 5

Authentication

Waffy uses OAuth2 with three distinct tokens. Each has a specific role. Confusing them is the most common integration mistake.

  1. 1Auth
  2. 2Users
  3. 3Contract
  4. 4Payment
  5. 5Settle

Before you start

Waffy provides these four credentials at onboarding. If you don't have them yet, stop here.

CredentialWhat it is
client_idYour unique client identifier
client_passwordYour client secret
admin_emailThe org owner's email
admin_passwordThe org owner's password

Sandbox base URLs

base_url   https://dev-api.waffyapp.com
auth_url   https://dev-auth.waffyapp.com

org_code is not something you need to store or pass manually — it is returned automatically when you log in as the org admin and is already embedded in your token context.

The three tokens

TokenGrant typeUsed for
app_tokenclient_credentialsUser sign-up and management
user_tokenpassword (org admin)Creating contracts, settlement, balance
customer_tokenpassword (buyer)Used in payment URL only

The seller never needs their own token in a headless flow. Your server acts on behalf of all parties using user_token — except payment, which is always scoped to the buyer.

Step 1 — Get your app_token

System-level token. Used for user management operations. Cache for 55 minutes (valid 60 min).

bash
curl "$WAFFY_AUTH_URL/oauth/token" \
  -u "$WAFFY_CLIENT_ID:$WAFFY_CLIENT_PASSWORD" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials&scope=WRITE"

Response:

json
{
  "access_token": "eyJhbGc...",
  "token_type": "bearer",
  "expires_in": 3600
}

Save as app_token.

Step 2 — Get your user_token

This token acts as your org admin — the one who creates contracts, settles, and manages the organization. Uses the admin_email and admin_password provided by Waffy at onboarding.

bash
curl "$WAFFY_AUTH_URL/oauth/token" \
  -u "$WAFFY_CLIENT_ID:$WAFFY_CLIENT_PASSWORD" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "grant_type=password" \
  --data-urlencode "username=$WAFFY_ADMIN_EMAIL" \
  --data-urlencode "password=$WAFFY_ADMIN_PASSWORD" \
  --data-urlencode "scope=WRITE"

Response shape is identical — save as user_token.

Getting a customer_token (at payment time)

When the buyer is ready to pay, fetch a short-lived token scoped to their identity. Use the clientUserToken returned from sign-up as their password in a password-grant call:

bash
curl "$WAFFY_AUTH_URL/oauth/token" \
  -u "$WAFFY_CLIENT_ID:$WAFFY_CLIENT_PASSWORD" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "grant_type=password" \
  --data-urlencode "username={{buyer_phone}}" \
  --data-urlencode "password=$BUYER_USER_PASSWORD" \
  --data-urlencode "scope=WRITE"

Where does the buyer's password come from?

The sign-up endpoint (Step 3) returns a clientUserToken in the response. That value is the buyer's Waffy password. Store it securely per user — encrypted at rest, never logged, never exposed to the browser. You'll use it here to get a fresh customer_token each time the buyer pays.

Token lifetime

  • All tokens are JWTs valid for 60 minutes (expires_in: 3600). No refresh token is issued.
  • Cache app_token and user_token server-side — renew on a schedule (e.g. every 55 min) to avoid races.
  • Obtain customer_token on demand — immediately before redirecting the buyer to checkout.

Error responses

HTTPCodeMeaningFix
401invalid_clientWrong client_id or client_password.Check Basic Auth header — must be base64 of id:password.
401invalid_grantWrong username or password in password grant.Re-check admin credentials or stored user password.
400unsupported_grant_typeTypo in grant_type.Must be exactly client_credentials or password.
400invalid_scopeScope not recognised.Use WRITE for integration flows.
429Too many token requests.Cache tokens server-side. Don't request on every API call.