Getting started
Environments, authentication and your first request.
Environments
| Environment | Base URL | What happens |
|---|---|---|
| Development | https://api.dev.b2b.heru.app/v1 | The same services and checks as production, on test data. CFDI are stamped by the SAT test environment and have no fiscal validity. Every operation labeled Live runs here today; see the development environment. |
| Production | https://api.b2b.heru.app/v1 | Real queries to the SAT, RENAPO and the IMSS, and real CFDI. The v1 routes open here with the gateway; your account team tells you when to switch. |
Your heru account team issues a client (client_id and client_secret) for each environment, with the scopes of your contract. The Try it of every operation in the API reference takes them: pick the environment, paste the client and press Get token. They stay in your browser tab only.
Real identities in development
With your development credentials, a real CURP or RFC runs a real validation against the SAT, RENAPO or the IMSS. Use real identities in development only with the person's consent, as you would in production.
v1 paths
Customers already in production reach the live capabilities at earlier paths. The /v1 routes open in production with the new gateway; your account team will tell you when to switch.
Authentication
The API uses OAuth 2.0 client credentials. Exchange your client_id and client_secret at /oauth/token of the environment for an access token, then send it on every request as Authorization: Bearer <token>.
- A token is valid for one hour. Request a new one when it expires; there is no refresh token.
- A token belongs to the environment that issued it: a development token is rejected in production and the other way round.
- Each credential carries scopes for the services in your contract, such as
workers:writeorinvoices:read. A call outside your scopes answers403. Every scope and the operations it allows are in Permissions. - You can keep two client secrets active at the same time, so you can rotate without downtime.
- Keep the secret on your servers. Never ship it in a mobile app or a web page.
Your first request
Three calls against development: get a token, register a worker, and read them back.
Get a token
curl -X POST https://api.dev.b2b.heru.app/oauth/token \
-d grant_type=client_credentials \
-d client_id=$HERU_CLIENT_ID \
-d client_secret=$HERU_CLIENT_SECRET{
"access_token": "eyJhbGciOiJFUzI1NiIs…",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "workers:read workers:write …"
}A wrong client answers 401 with "error": "invalid_client"; a scope outside your contract answers 400 with "error": "invalid_scope".
Register a worker
POST /workers needs an Idempotency-Key, like almost every POST; see Idempotency and retries. The CURP below is synthetic: it has the shape of a real one and belongs to nobody.
curl -X POST https://api.dev.b2b.heru.app/v1/workers \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: DRV-000088213" \
-H "Content-Type: application/json" \
-d '{"external_id": "DRV-000088213", "curp": "HEGL900101HDFRRS06", "services": ["fiscal_validation", "invoicing"]}'The answer is 201 Created with the worker pending and the enrollment link heru sends them. Keep the id: it is how you read the worker, and what you send as receiver.worker_id on their retention certificates and third_party.worker_id on their invoices.
{
"id": "wrk_01m4ra8w0he5esnnsew0v9ffkv",
"external_id": "DRV-000088213",
"status": "pending",
"curp": "HEGL900101HDFRRS06",
"services": ["fiscal_validation", "invoicing"],
"open_actions": ["give_consent", "upload_efirma"],
"enrollment": {
"status": "pending",
"url": "https://enroll.heru.app/w/Rk7pX2mQ9vLc4TzN8bWy?expires=1792160571&signature=9f2c7a41e8",
"expires_at": "2026-10-16T14:22:51.604Z",
"completed_at": null
},
"created_at": "2026-10-09T14:22:51.604Z"
}Read the worker
Read one worker by id, or list them with GET /workers, filtering by external_id or status. In production, a webhook tells you when their consent, e.firma or status changes.
curl https://api.dev.b2b.heru.app/v1/workers/wrk_01m4ra8w0he5esnnsew0v9ffkv \
-H "Authorization: Bearer $TOKEN"status stays pending until the worker completes the enrollment; open_actions lists what is still missing. From here, the Platforms with workers guide takes you to their invoices and retention certificates.
Or send the same calls from the Try it of POST /workers and GET /workers/{id}, with your development credentials.
Conventions
| Asynchronous work | Validations with the SAT, RENAPO or the IMSS answer 202 as soon as heru accepts the request. The result arrives by webhook; you can also read it with GET. Invoices and retention certificates are the exception: they are stamped while you wait and answer 201. |
| Idempotency | Almost every POST requires an Idempotency-Key; the reference says which take none. A retry with the same key returns the original result for 24 hours, so a network error never creates a second invoice. More. |
| Your ids | Send external_id with your own id. It comes back when you read the resource and in every webhook, and you can filter lists by it. |
| Money | MXN as strings with two decimals: "1250.00". |
| Dates | ISO 8601. Timestamps are in UTC unless you send an offset. |
| Pagination | Lists return data and next_cursor; pass it back as cursor. More. |
| Errors | JSON with a stable code, a message, a link to its docs page and, for validation errors, the fields that failed. Branch on code. More. |
| Rate limits | Per credential, as agreed in your contract. Current usage comes in the RateLimit headers; a 429 carries Retry-After. |
| Versioning | The version is in the path. We add fields without notice, so ignore the ones you do not know. More. |
| Personal data | We never ask for card numbers. Signed file links expire after 24 hours. |