Asesia API v2

A versioned REST + JSON API for accounting firms and companies that connect their own tools: ERPs, online shops, CRMs and in-house software. Everything the API does goes through the same services as the app — the same VAT rules, quotas, VERI*FACTU / TicketBAI / SII issuing and document sanitisation.

Machine-readable specification (OpenAPI 3.1)

Authentication

  • Create credentials in Settings → API v2 (firm administrators, or the company owner when the firm allows it). Secrets are shown once and stored hashed; rotate them with a 24-hour grace period, or invalidate them at once if you suspect a leak. Every token issued, every write and (grouped) every read is audited.
  • OAuth 2.0 client credentials: exchange client_id + client_secret for a 1-hour access token. Ask for a subset of the client's scopes with scope.
  • Personal API keys (asesia_pk_…) are sent directly as a Bearer token, with the scopes fixed at creation.
  • A firm credential reaches the firm's companies (same market only) and only the sections each company shares with its firm. A company credential reaches its own company.
curl -u "$CLIENT_ID:$CLIENT_SECRET" \
  -d grant_type=client_credentials -d "scope=invoices:read invoices:write" \
  https://<host>/api/v2/oauth/token

curl -H "Authorization: Bearer $ACCESS_TOKEN" \
  https://<host>/api/v2/companies

companies:read · documents:read · documents:write · captures:read · expenses:read · invoices:read · invoices:write · invoices:issue · contacts:read · contacts:write · taxes:read · bank:read · webhooks:manage

Conventions

  • Every resource lives under /companies/{companyId}. Ids of companies or objects you cannot reach answer 404, exactly like ids that do not exist.
  • Pagination: limit (1–200, default 50) and an opaque cursor. Lists return data, total, truncated and nextCursor.
  • Idempotency: send Idempotency-Key on POST creations. A retry returns the same response for 24 h (Idempotent-Replayed: true); the same key with a different body is rejected with 422.
  • Errors: { "error": { "code", "params"?, "detail"?, "requestId" } }. Program against code (a stable catalogue shared with the app); detail is a hint and may be localized. Every response carries X-Request-Id.
  • Rate limits are per credential (X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After on 429).
  • Reads carry a weak ETag; send it back as If-None-Match to get 304 Not Modified.
  • Amounts are in EUR; invoices also keep their original currency.

Endpoints

Authentication

post/api/v2/oauth/token

public

Get an access token (OAuth 2.0 client credentials)

Authenticate the client with HTTP Basic (client_id:client_secret, preferred) or client_secret_post. Tokens last 1 hour. Errors follow RFC 6749 §5.2.

Companies

get/api/v2/companies

scope companies:read

List the companies this credential reaches

get/api/v2/companies/{companyId}

scope companies:read

Get a company

Documents

get/api/v2/companies/{companyId}/documents

scope documents:read

List uploaded documents and their processing status

post/api/v2/companies/{companyId}/documents

scope documents:write

Upload a document

multipart/form-data, field document. Same pipeline as the app: content-type and magic bytes, content disarm & reconstruction, antivirus, storage and OCR quotas. Answers at once; OCR runs in the background (poll GET …/documents/{id} or subscribe to document.processed).

Supports Idempotency-Key

get/api/v2/companies/{companyId}/documents/{id}

scope documents:read

Get the status of a document

Captures

get/api/v2/companies/{companyId}/captures

scope captures:read

List captures with extracted data and review status

get/api/v2/companies/{companyId}/captures/{id}

scope captures:read

Get a capture

Expenses

get/api/v2/companies/{companyId}/expenses

scope expenses:read

List expenses

get/api/v2/companies/{companyId}/expenses/{id}

scope expenses:read

Get an expense

Invoices

get/api/v2/companies/{companyId}/invoices

scope invoices:read

List issued invoices

post/api/v2/companies/{companyId}/invoices

scope invoices:write

Create a draft invoice

Same validation as the app (VAT rates of the company's country, contact ownership, currency, withholding, exemption grounds, plan quota).

Supports Idempotency-Key

get/api/v2/companies/{companyId}/invoices/{id}

scope invoices:read

Get an invoice with its lines and VERI*FACTU status

post/api/v2/companies/{companyId}/invoices/{id}/issue

scope invoices:issue

Issue a draft (irreversible)

Exactly the same issuing service as the "Issue" button: series number and, where the company has it, the VERI*FACTU record in the same transaction; then TicketBAI / SII as configured. If anything fails the invoice stays a draft and no number is used. Follow the tax-authority verdict with the invoice.verifactu_status webhook.

Supports Idempotency-Key

Contacts

get/api/v2/companies/{companyId}/contacts

scope contacts:read

List contacts (customers and suppliers)

post/api/v2/companies/{companyId}/contacts

scope contacts:write

Create a contact

Supports Idempotency-Key

get/api/v2/companies/{companyId}/contacts/{id}

scope contacts:read

Get a contact

patch/api/v2/companies/{companyId}/contacts/{id}

scope contacts:write

Update a contact

Taxes

get/api/v2/companies/{companyId}/tax-calendar

scope taxes:read

Upcoming tax deadlines of the company

get/api/v2/companies/{companyId}/tax-models

scope taxes:read

Status of tax returns by period (pending / prepared / filed)

get/api/v2/companies/{companyId}/tax-models/{model}

scope taxes:read

Calculated result of an AEAT model for a period (ES)

Read-only calculation from the company's data; does not file anything. Models: 303, 130, 111, 115, 180, 190, 390, 123, 193, 202, 200.

Bank

get/api/v2/companies/{companyId}/bank-movements

scope bank:read

List bank movements

Webhooks

get/api/v2/webhooks

scope webhooks:manage

List webhook endpoints of the credential's owner

post/api/v2/webhooks

scope webhooks:manage

Register a webhook endpoint

https only; the host must resolve to public IPv4 addresses (checked again, pinned, on every delivery). The signing secret is returned ONCE.

Supports Idempotency-Key

get/api/v2/webhooks/{id}

scope webhooks:manage

Get a webhook endpoint

patch/api/v2/webhooks/{id}

scope webhooks:manage

Update, pause or resume a webhook endpoint

delete/api/v2/webhooks/{id}

scope webhooks:manage

Delete a webhook endpoint

get/api/v2/webhooks/{id}/deliveries

scope webhooks:manage

Delivery log

post/api/v2/webhooks/{id}/deliveries/{deliveryId}/redeliver

scope webhooks:manage

Redeliver an event (same event id)

Webhooks

  • Register https endpoints (POST /webhooks or Settings). The host must resolve to public IPv4 addresses; this is checked when you save it and again, pinned to the connected address, on every delivery (no private, loopback or link-local destinations, no redirects).
  • Each delivery is signed: X-Asesia-Signature: v1=<hex HMAC-SHA256 of timestamp + "." + raw body> with your endpoint secret, plus X-Asesia-Timestamp (seconds). Reject anything older than 5 minutes.
  • Deduplicate on X-Asesia-Event-Id (the id in the body): it is the same across retries and manual redeliveries.
  • Failed deliveries are retried after 1, 5, 30, 120 and 360 minutes (408, 429 and 5xx; other 4xx are not retried). 15 consecutive failures pause the endpoint. The delivery log and manual redelivery are in the API and in Settings.
  • Firm endpoints receive events only for what each company shares with its firm.
import crypto from 'node:crypto'

function verify(rawBody, secret, signature, timestamp) {
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false
  const mac = crypto.createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex')
  const a = Buffer.from(`v1=${mac}`), b = Buffer.from(signature)
  return a.length === b.length && crypto.timingSafeEqual(a, b)
}

Events

document.received
A document entered the company (upload, email, WhatsApp, API).
document.processed
OCR finished: extracted data is available.
capture.approved
A human (or an approval rule) approved a captured document; it is now an accounting fact.
capture.rejected
A captured document was rejected.
expense.created
An expense was created.
invoice.created
A sales invoice (draft or issued) was created.
invoice.issued
A draft was issued: it has its series number (and, where enabled, its tax-authority record).
invoice.paid
An invoice was marked as paid.
invoice.verifactu_status
The VERI*FACTU record of an invoice changed state (accepted, accepted with errors, rejected).
model.ready
A tax return was prepared or filed for a period (status PREPARADO or PRESENTADO).
bank.reconciled
A bank movement was reconciled.

Migrating from v1

  • /api/v1 keeps working unchanged. It is deprecated: new features land only in v2.
  • v1 keys (asesia_sk_…) are not accepted by v2 and v2 credentials are not accepted by v1. Create a v2 credential with the equivalent scopes, switch the base URL to /api/v2, and move the company from the X-Workspace-Id header into the path (/companies/{companyId}/…).
  • Field names change from the internal names to the v2 contract (e.g. nif → taxId, next_cursor → nextCursor), and errors move into the error object.
  • v1 webhooks keep firing; re-register your endpoints as v2 webhooks to get the new events (invoice.issued, invoice.verifactu_status, model.ready…), pinned SSRF protection and the delivery log. Then delete the v1 endpoint to avoid duplicates.
  • When every integration has moved, revoke the v1 keys.