Getting Started · Step 1 of 5

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.

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

Before you start

You need an API client for your organization and its admin login:

CredentialWhat it is
client_idYour organization’s API client
client_secretIts secret. Shown only once, when the client is created in the business portal (Settings, API clients)
org_admin_usernameLogin of your organization admin, the same as in the business portal
org_admin_passwordThat 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.

EnvironmentWaffy ID (tokens)APICheckout
Dev (sandbox)https://id-dev.waffyapp.comhttps://dev-api.waffyapp.comhttps://external-dev.waffyapp.com
Stghttps://id-stg.waffyapp.comhttps://api-stg.waffyapp.comhttps://external-stg.waffyapp.com
Prodhttps://id.waffyapp.comhttps://api.waffyapp.comhttps://external.waffyapp.com

The examples below use $ID_BASE_URL for the host of Waffy ID in your environment.

Which token for what

TokenHow you get itUsed for
org tokenclient_credentials with your clientRegistering or linking customers, OTP and consent calls, bank account and address updates
client admin tokenpassword grant with your org admin's loginContracts, milestones, settlement, withdrawals, balance, and the checkout start URL
payment ticketToken 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

bash
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"
json
{
  "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:

bash
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_id must belong to your own organization. A client from another organization fails with 400 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

This is the one call where a real person's password is sent directly. Store it encrypted, never log it and never expose it to a browser. Talk to Waffy if this does not fit your integration.

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:

bash
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"
json
{ "ticket": "a1b2c3...", "expiresIn": 300 }
  • PAYMENT_REF is the contract's shortId, the same value that is in the checkout URL path (.../external/6VzpzZlR?... means 6VzpzZlR). 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

The 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

BearerRequests
Org tokenRegister or link a customer, send consent link, send and verify OTP, update bank account, add address
Client admin tokenCreate 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

HTTPCodeMeaningFix
400unauthorized_clientPassword login is not enabled for this client.Ask Waffy to enable it for the client.
400 / 401invalid_grantWrong 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.
400consent_rejectedPayment ticket: the customer declined your organization.A fresh consent link, OTP verification or checkout login can reverse it. Then retry.
400invalid_grantPayment 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

Organizations that integrated before the Partner API use /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 tokenGrantReplaced by
app_tokenclient_credentialsOrg token (step 1)
user_tokenpassword (org admin)Client admin token (step 2)
customer_tokenpassword (buyer)Payment ticket (step 3)
bash
# 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.