Send WhatsApp payment reminders and invoice alerts from Xero. OAuth 2.0 with a 30-minute access token, the Xero-tenant-Id header on every call, signed webhooks that deliver only metadata (then you fetch the full record), and overdue invoice queries covered in full.
Xero access tokens have a 30-minute lifetime, the shortest of any accounting API in this series. You must refresh before each batch of calls or implement automatic refresh on 401 responses. Request the offline_access scope during authorisation to receive a refresh token (valid 60 days).
When Xero fires a webhook for an invoice event, the payload contains only the InvoiceID, event type, and tenant ID. The invoice details, customer name, amount, and contact phone are not included. Your automation must call GET /api.xro/2.0/Invoices/{InvoiceID} to fetch the full record before sending the WhatsApp.
https://localhost for self-service setup).https://login.xero.com/identity/connect/authorize ?response_type=code &client_id=YOUR_CLIENT_ID &redirect_uri=https://localhost &scope=openid profile email accounting.transactions accounting.contacts offline_access &state=random123 After approval, Xero redirects to: https://localhost?code=AUTHORISATION_CODE&state=random123 Exchange the code for tokens:
POST https://identity.xero.com/connect/token
Authorization: Basic base64(CLIENT_ID:CLIENT_SECRET)
Content-Type: application/x-www-form-urlencoded
Body:
grant_type=authorization_code
&code=AUTHORISATION_CODE
&redirect_uri=https://localhost
Response:
{
"access_token": "eyJhbGciOiJSUzI1...",
"refresh_token": "c4ece16d-d...",
"expires_in": 1800,
"token_type": "Bearer"
}
expires_in is 1800 seconds (30 minutes). Refresh before it expires.Xero developer documentation: developer.xero.com
The Xero-tenant-Id header is required on every API call.
Retrieve it from the connections endpoint immediately after authorisation.
GET https://api.xero.com/connections
Authorization: Bearer YOUR_ACCESS_TOKEN
Response:
[
{
"id": "abc123-def456",
"tenantId": "45e4708e-d862-4111-ab3a-dd8cd03913e1",
"tenantName": "ABC Traders",
"tenantType": "ORGANISATION"
}
]
Store tenantId. Add to every subsequent call:
Xero-tenant-Id: 45e4708e-d862-4111-ab3a-dd8cd03913e1POST https://identity.xero.com/connect/token Authorization: Basic base64(CLIENT_ID:CLIENT_SECRET) Content-Type: application/x-www-form-urlencoded Body: grant_type=refresh_token &refresh_token=YOUR_REFRESH_TOKEN Response: new access_token (30-minute lifetime). Refresh token is rotated: store the new refresh token from the response.
Every time you refresh the access token, Xero returns a new refresh token alongside the new access token. Store the new refresh token each time. The previous one is invalidated. If you use an old refresh token, the connection is revoked and you must re-authorise.
x-xero-signature header.POST to your WA.Expert webhook URL:
Headers:
x-xero-signature: HMAC-SHA256 of raw body using your Webhooks Key
Body:
{
"events": [
{
"resourceUrl": "https://api.xero.com/api.xro/2.0/Invoices/a1b2c3d4",
"resourceId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"eventDateUtc": "2026-06-23T21:30:00.000Z",
"eventType": "CREATE",
"eventCategory":"INVOICE",
"tenantId": "45e4708e-d862-4111-ab3a-dd8cd03913e1"
}
]
}
This is METADATA ONLY. No invoice amount, no customer name, no phone.
Follow up with GET /Invoices/{resourceId} to get full details.GET https://api.xero.com/api.xro/2.0/Invoices/a1b2c3d4-e5f6-7890-abcd-ef1234567890
Authorization: Bearer YOUR_ACCESS_TOKEN
Xero-tenant-Id: 45e4708e-d862-4111-ab3a-dd8cd03913e1
Accept: application/json
Response (simplified):
{
"Invoices": [{
"InvoiceID": "a1b2c3d4...",
"InvoiceNumber": "INV-0142",
"Status": "AUTHORISED",
"AmountDue": 48500.00,
"DueDateString": "2026-07-07",
"Contact": {
"Name": "Priya Textiles",
"Phones": [
{"PhoneType": "MOBILE", "PhoneNumber": "9820012345"}
]
}
}]
}
Map:
Contact.Name -> party_name
"+91" + Phones[MOBILE] -> wa_phone <- PREPEND +91
InvoiceNumber -> invoice_number
AmountDue -> amount_due
DueDateString -> due_dateGET https://api.xero.com/api.xro/2.0/Invoices
?Statuses=AUTHORISED
&Type=ACCREC
&where=AmountDue%3E0
Authorization: Bearer YOUR_ACCESS_TOKEN
Xero-tenant-Id: 45e4708e-d862-4111-ab3a-dd8cd03913e1
Accept: application/json
Alternatively filter by date:
&DueDateFrom=2026-01-01&DueDateTo=2026-06-23
Response: { Invoices: [{ InvoiceNumber, AmountDue, DueDateString,
Contact: { Name, Phones } }] }
For each invoice where AmountDue > 0 and DueDateString < today:
Send WhatsApp payment reminder to Contact mobile.| Symptom | Likely cause | Fix |
|---|---|---|
| 401 on API calls | Access token expired (30-minute lifetime) | Refresh the access token using the refresh token. Implement auto-refresh on 401. |
| Webhook subscription not activating | Intent to Receive ping failed | Your endpoint must respond 200 within 5 seconds to the initial Intent to Receive ping. Ensure the WA.Expert automation is published and responding. |
| x-xero-signature mismatch | Signature verification failing | Compute HMAC-SHA256 of the raw (unparsed) request body using your webhook signing key. Compare to the x-xero-signature header value. Return 401 on mismatch. |
| No invoice data in webhook payload | Xero webhooks are metadata-only | After receiving the webhook, call GET /Invoices/{resourceId} to fetch the full record. |
| Refresh token invalid | Old refresh token used after rotation | Xero rotates refresh tokens on each use. Always store and use the new refresh token returned in the refresh response. |
| Xero-tenant-Id missing | Header omitted | Every Xero API call requires the Xero-tenant-Id header. Retrieve it once from GET /connections and include it on all subsequent calls. |
Zoho Books: OAuth 2.0, India domain (.in), workflow webhooks for invoices.
Read guideERPNext: token auth, DocType API, built-in webhook on invoice submit.
Read guideFree trial, no credit card. If you get stuck, we answer live on WhatsApp.