Authentication
Waffy uses OAuth2 with three distinct tokens. Each has a specific role. Confusing them is the most common integration mistake.
- 1Auth
- 2Users
- 3Contract
- 4Payment
- 5Settle
Before you start
Waffy provides these four credentials at onboarding. If you don't have them yet, stop here.
| Credential | What it is |
|---|---|
client_id | Your unique client identifier |
client_password | Your client secret |
admin_email | The org owner's email |
admin_password | The 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
| Token | Grant type | Used for |
|---|---|---|
app_token | client_credentials | User sign-up and management |
user_token | password (org admin) | Creating contracts, settlement, balance |
customer_token | password (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).
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:
{
"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.
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:
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_tokenanduser_tokenserver-side — renew on a schedule (e.g. every 55 min) to avoid races. - •Obtain
customer_tokenon demand — immediately before redirecting the buyer to checkout.
Error responses
| HTTP | Code | Meaning | Fix |
|---|---|---|---|
| 401 | invalid_client | Wrong client_id or client_password. | Check Basic Auth header — must be base64 of id:password. |
| 401 | invalid_grant | Wrong username or password in password grant. | Re-check admin credentials or stored user password. |
| 400 | unsupported_grant_type | Typo in grant_type. | Must be exactly client_credentials or password. |
| 400 | invalid_scope | Scope not recognised. | Use WRITE for integration flows. |
| 429 | — | Too many token requests. | Cache tokens server-side. Don't request on every API call. |