Authentication
Your organization authenticates with OAuth2 against Waffy's Waffy ID. Which token you use depends on what you are doing, and mixing them up is the most common integration mistake.
- 1Auth
- 2Users
- 3Contract
- 4Payment
- 5Settle
Before you start
You need an API client for your organization and its admin login:
| Credential | What it is |
|---|---|
client_id | Your organization’s API client |
client_secret | Its secret. Shown only once, when the client is created in the business portal (Settings, API clients) |
org_admin_username | Login of your organization admin, the same as in the business portal |
org_admin_password | That admin’s password |
Don't have a client yet? Create one in the business portal, or contact Waffy. Each environment needs its own client and credentials. The Postman collection comes with an environment file already filled in for your client.
| Environment | Waffy ID (tokens) | API | Checkout |
|---|---|---|---|
| Dev (sandbox) | https://id-dev.waffyapp.com | https://dev-api.waffyapp.com | https://external-dev.waffyapp.com |
| Stg | https://id-stg.waffyapp.com | https://api-stg.waffyapp.com | https://external-stg.waffyapp.com |
| Prod | https://id.waffyapp.com | https://api.waffyapp.com | https://external.waffyapp.com |
The examples below use $ID_BASE_URL for the host of Waffy ID in your environment.
Which token for what
| Token | How you get it | Used for |
|---|---|---|
org token | client_credentials with your client | Registering or linking customers, OTP and consent calls, bank account and address updates |
client admin token | password grant with your org admin's login | Contracts, milestones, settlement, withdrawals, balance, and the checkout start URL |
payment ticket | Token exchange for one customer and one payment (customer-token-exchange) | A one-time value you append to the checkout redirect. It is not a bearer token |
A client admin token carries the CLIENT_ADMIN role, which neither the org token nor a customer can have. An endpoint that needs it answers 403 to any other token.
Step 1: Get an org token
curl -s -X POST "$ID_BASE_URL/realms/waffy/protocol/openid-connect/token" \ -H "Accept: application/json" \ --data-urlencode "grant_type=client_credentials" \ --data-urlencode "client_id=$CLIENT_ID" \ --data-urlencode "client_secret=$CLIENT_SECRET"
{
"access_token": "eyJhbGc...",
"expires_in": 600,
"scope": "EXTERNAL"
}Send it as Authorization: Bearer <access_token>. Cache it on your server and renew it shortly before expires_in runs out. Do not request a new token for every call.
Step 2: Get a client admin token
Needed for the business calls (contracts, settlement, balance). It is a normal API call, with no browser step, so it works from a backend:
curl -s -X POST "$ID_BASE_URL/realms/waffy/protocol/openid-connect/token" \ -H "Accept: application/json" \ --data-urlencode "grant_type=password" \ --data-urlencode "client_id=$CLIENT_ID" \ --data-urlencode "client_secret=$CLIENT_SECRET" \ --data-urlencode "username=$ORG_ADMIN_USERNAME" \ --data-urlencode "password=$ORG_ADMIN_PASSWORD"
client_idmust belong to your own organization. A client from another organization fails with400 invalid_grant.- A contract you create is attributed to the client you signed in with, so use the same client for the checkout start URL in that session.
Keep the admin password on your server
Step 3: Get a payment ticket (at payment time)
When a customer who has given consent is ready to pay, your backend exchanges your client for a short-lived ticket for that customer and that payment:
curl -s -X POST "$ID_BASE_URL/realms/waffy/protocol/openid-connect/token" \ -H "Accept: application/json" \ --data-urlencode "grant_type=urn:waffy:params:oauth:grant-type:customer-token-exchange" \ --data-urlencode "client_id=$CLIENT_ID" \ --data-urlencode "client_secret=$CLIENT_SECRET" \ --data-urlencode "login_hint=$CUSTOMER_PHONE" \ --data-urlencode "scope=payment:$PAYMENT_REF"
{ "ticket": "a1b2c3...", "expiresIn": 300 }PAYMENT_REFis the contract'sshortId, the same value that is in the checkout URL path (.../external/6VzpzZlR?...means6VzpzZlR). It is not an internal id or an order reference of your own.- You then redirect the customer's browser to the checkout URL with
&ticket=...appended. Waffy's checkout exchanges it for a real token itself, so your integration ends at the redirect. - The ticket is valid for 5 minutes. Request it right before the redirect.
Availability
customer-token-exchange grant is not enabled in every environment yet. Confirm with Waffy which environments have it before relying on it outside dev.Which token each call needs
| Bearer | Requests |
|---|---|
| Org token | Register or link a customer, send consent link, send and verify OTP, update bank account, add address |
| Client admin token | Create contract, create milestone, add parties, get contract details and milestones, accept, reject and settle contract, create withdrawal, balance and balance history, start payment URL |
Error responses
| HTTP | Code | Meaning | Fix |
|---|---|---|---|
| 400 | unauthorized_client | Password login is not enabled for this client. | Ask Waffy to enable it for the client. |
| 400 / 401 | invalid_grant | Wrong admin username or password, a locked or disabled account, or a client that belongs to another organization. | Check the credentials and use one of your own organization’s clients. |
| 400 | consent_rejected | Payment ticket: the customer declined your organization. | A fresh consent link, OTP verification or checkout login can reverse it. Then retry. |
| 400 | invalid_grant | Payment ticket: the customer is not linked to this client. | Register or link the customer first. |
| 401 | — | Missing, expired or malformed token, or the wrong kind of token (for example a client admin token on a customer endpoint). | Get a fresh org token and use the token the endpoint expects. |
| 403 | — | The token is not a CLIENT_ADMIN token. | Use the client admin token from step 2. |
| 429 | — | Your organization’s hourly OTP or consent-link limit was reached. | Retry after the window resets, or ask Waffy to raise the limit for legitimate traffic. |
Legacy authentication (deprecated)
Deprecated: do not use for new integrations
/oauth/token on the authentication service with an app_token, user_token and customer_token. It keeps working while those organizations move to the flow above. Waffy will announce a retirement date. If you are still on it, plan the move and contact Waffy for help.| Legacy token | Grant | Replaced by |
|---|---|---|
app_token | client_credentials | Org token (step 1) |
user_token | password (org admin) | Client admin token (step 2) |
customer_token | password (buyer) | Payment ticket (step 3) |
# app_token (legacy) 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" # user_token (legacy) 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"
Legacy tokens are JWTs valid for 60 minutes (expires_in: 3600) and no refresh token is issued. The legacy customer_token is obtained with a password grant using the buyer's clientUserToken from sign-up, and is used in the payment URL only.