REST API, AJAX & webhooks
Easy Invoice has three HTTP surfaces:
- REST API at
/wp-json/easy-invoice/v1/— the one to build integrations on. Added in 2.4.0. admin-ajax.phpfor the plugin's own in-page AJAX (search, save, send, mark paid, generate PDF). Internal; expect it to change.- Gateway webhooks for asynchronous payment notifications (Stripe, Mollie, Square, Authorize.Net) — also routed through
admin-ajax.php.
If you are inside the same WordPress install, skip HTTP and call the PHP services in-process (see the end of this page).
REST API
Base URL: https://yoursite.com/wp-json/easy-invoice/v1/
Authentication
Every route needs a signed-in user — there are no public routes. Use any standard WordPress REST authentication:
- Application passwords (WordPress 5.6+): Users → Profile → Application Passwords. Send them as HTTP Basic auth. This is the right choice for mobile apps, headless front-ends and server-to-server calls.
- Cookie + nonce from inside a logged-in browser session (pass the
X-WP-Nonceheader).
Each route checks the same ei_* capability as the matching admin screen, so a user with the EI Viewer role gets read access and nothing more, and the Team Roles addon applies unchanged. An unauthenticated call gets 401 easy_invoice_rest_unauthenticated; a signed-in user without the capability gets 403 easy_invoice_rest_forbidden.
Routes
| Route | Method | Capability | Purpose |
|---|---|---|---|
/invoices | GET | ei_view_invoices | List invoices (page, per_page ≤ 100, search). |
/invoices | POST | ei_create_invoice | Create an invoice. |
/invoices/{id} | GET | ei_view_invoices | One invoice. |
/invoices/{id} | DELETE | ei_delete_invoice | Move an invoice to the trash. |
/invoices/{id}/pdf | GET | ei_view_invoices | The invoice as a PDF file (application/pdf, server-rendered). |
/quotes | GET | ei_view_quotes | List quotes (page, per_page, search). |
/quotes/{id} | GET | ei_view_quotes | One quote. |
/clients | GET | ei_view_clients | List clients (page, per_page, search). |
List responses carry the usual X-WP-Total and X-WP-TotalPages headers. An id that does not exist — or belongs to a different kind of document — is a 404 easy_invoice_rest_not_found.
DELETE trashes rather than erases, exactly as the admin screen does: an invoice is a financial record, and an issued one cannot be deleted permanently by any route.
Invoice representation
{
"id": 20,
"number": "INV-000002",
"title": "Web Design",
"status": "available",
"viewed": { "count": 2, "first": "2026-09-01 10:12:00", "last": "2026-09-10 08:40:00" },
"issue_date": "2026-08-01",
"due_date": "2026-08-31",
"customer": { "name": "Rigel Vaughn", "email": "[email protected]", "country": "NP", "vat": "" },
"totals": {
"subtotal": 430, "discount": 0, "tax": 0, "total": 430,
"paid": 0, "credited": 0, "due": 430
},
"items": [
{ "name": "Design", "description": "", "quantity": 1, "price": 430, "amount": 430, "taxable": true }
],
"links": { "pdf": "https://yoursite.com/wp-json/easy-invoice/v1/invoices/20/pdf" },
"tax_treatment": { "category": "standard", "statement": "" }
}status is the stored workflow status (draft, available, partial, paid, unpaid, cancelled); whether an invoice is overdue is derived from due_date and totals.due, just as the admin list does. totals.paid sums completed payments, totals.credited sums credit notes, and totals.due is what is still owed.
The per-document access key that makes the public payment link work is a bearer credential and is never included in a response.
Creating an invoice
curl -u "apiuser:xxxx xxxx xxxx xxxx xxxx xxxx" \
-H "Content-Type: application/json" \
-d '{
"title": "Web Design",
"client_id": 21,
"due_date": "2026-10-01",
"terms": "Net 30",
"items": [ { "name": "Design", "quantity": 1, "price": 430 } ]
}' \
https://yoursite.com/wp-json/easy-invoice/v1/invoices| Field | Notes |
|---|---|
items | Required. Each item takes name, description, quantity, price, taxable. |
client_id | A WordPress user id; the invoice's customer name and email are filled from the client record. Unknown id → 400 easy_invoice_rest_no_client. |
customer_name, customer_email | Used when there is no client record, or to override it. |
status | draft (default) or available. Payment statuses cannot be set here — record a payment instead. |
number | Leave it out to take the next number in your sequence. |
issue_date, due_date | Y-m-d. |
notes, terms, currency, customer_vat_number, customer_country | Optional. |
The invoice is created through the same repository the invoice builder uses, so it is numbered, given an access key, and announced on easy_invoice_invoice_created — Webhooks, Recurring Invoices and Smart Reminders see it like any other. The response is 201 with the full representation.
Shaping responses
Two filters let a plugin add fields (or strip them) without touching the controller:
add_filter( 'easy_invoice_rest_invoice', function ( array $data, $invoice ) {
$data['project'] = get_post_meta( $invoice->getId(), '_my_project_code', true );
return $data;
}, 10, 2 );
add_filter( 'easy_invoice_rest_quote', function ( array $data, $quote ) {
return $data;
}, 10, 2 );admin-ajax actions
The plugin's own screens talk to admin-ajax.php — wp_ajax_easy_invoice_* actions such as easy_invoice_save_invoice, easy_invoice_save_quote, easy_invoice_send_invoice_email, easy_invoice_send_quote_email, easy_invoice_delete_invoice, easy_invoice_search_clients and easy_invoice_get_report_data. They are nonce-protected, capability-checked and shaped around the admin UI, and they change between releases without notice. Build integrations on the REST API instead.
Two are visitor-facing, token-gated, and reached by the Accept / Decline buttons on the public quote page: easy_invoice_accept_quote and easy_invoice_decline_quote. Call them through the page, not directly — the Quote E-Signatures addon adds a signature to the same request.
Gateway webhooks
Each Pro gateway registers a webhook hook so the gateway can call back when a payment status changes asynchronously.
Stripe
POST https://yoursite.com/wp-admin/admin-ajax.php?action=easy_invoice_stripe_webhook- Configure in Stripe dashboard → Developers → Webhooks → Add endpoint.
- Events to subscribe:
payment_intent.succeeded,payment_intent.payment_failed,charge.refunded. - Stripe sends a
Stripe-Signatureheader; Easy Invoice verifies it against your Signing secret.
Mollie
POST https://yoursite.com/wp-admin/admin-ajax.php?action=easy_invoice_mollie_webhook- Mollie posts a
payment_id; Easy Invoice fetches the payment via Mollie's API to verify status.
Square
POST https://yoursite.com/wp-admin/admin-ajax.php?action=easy_invoice_square_webhook- Configure in Square dashboard → Apps → Webhook subscriptions.
- Events:
payment.created,payment.updated,refund.created. - A 15-minute reconcile cron (
easy_invoice_check_pending_square_payments) fills in any webhook the listener missed.
Authorize.Net
POST https://yoursite.com/wp-admin/admin-ajax.php?action=easy_invoice_authorizenet_webhook- Configure in the Authorize.Net merchant interface → Account → Business Settings → Webhooks, and paste the Signature Key into the gateway settings — a webhook is rejected until it is set.
- Easy Invoice verifies the
X-ANET-Signatureheader (HMAC-SHA512) against that key.
Paystack
POST https://yoursite.com/wp-admin/admin-ajax.php?action=easy_invoice_paystack_webhook- Configure in Paystack dashboard → Settings → API Keys & Webhooks.
- Verified with HMAC-SHA512 of the raw body against your secret key (
x-paystack-signature).
Bank Transfer / Cheque / Cash / Manual
These have no webhooks — the admin marks the Payment record as completed manually after the funds arrive (see Payments).
Authentication notes
- REST API: application password (Basic auth) or cookie +
X-WP-Nonce. - admin-ajax actions: WordPress cookie + the nonce the admin script localizes.
- Gateway webhooks: each gateway uses its own signature scheme (Stripe and Authorize.Net headers, Paystack HMAC, Mollie API look-up). Easy Invoice handles verification.
Programmatic alternative: in-process PHP
If you're inside the same WordPress install (custom plugin / theme code), skip HTTP entirely:
$repository = \EasyInvoice\Providers\InvoiceServiceProvider::getInvoiceRepository();
// Create an invoice — numbered, hooked and filled from the client, like the builder does
$invoice = $repository->create( [
'title' => 'Web Design',
'client_id' => 21,
'status' => 'draft',
'items' => [ [ 'name' => 'Design', 'quantity' => 1, 'price' => 430 ] ],
] );
// Read one
$invoice = $repository->find( $invoice_id );
$total = $invoice->getTotal();
$due = \EasyInvoice\Services\InvoiceBalance::due( $invoice );
// React to money arriving (fires for full and partial payments)
add_action( 'easy_invoice_payment_completed', function ( $invoice_id, $invoice, $payment ) { /* settled */ }, 10, 3 );
add_action( 'easy_invoice_payment_received', function ( $invoice_id, $invoice, $payment ) { /* partial */ }, 10, 3 );This is faster, doesn't need authentication, and respects every filter/hook automatically.
Rate limits & caching
- The REST API and
admin-ajax.phpare uncached by default (most caching plugins exclude both). - No built-in rate limit — protect with Cloudflare / WP Hide / Login Lockdown if you expose webhooks publicly.
Where to go next
- 🪝 Hooks & filters — extend behaviour in-process.
- 💳 Payment gateways — gateway-specific webhook URLs.
- 🛠️ Troubleshooting → Webhooks.