Skip to main content

Conventions

This page describes cross-cutting rules of the API. Read it once and it applies everywhere.

Document identifiers

Each DTE issued via the API has three identifiers, designed for different purposes:

IdentifierAssigned byFormatTypical use
document_idOcoteUUID v4Internal API identifier. Returned in the response.
external_refYouFree text (max 100 chars)Identifier you control. Idempotency and retry.
control_numberMH (fixed by Ocote)DTE-{type}-{branch}{pos}-{15 digits}Fiscal correlative. Appears in the signed JSON and the ticket.

Important rules

  • external_ref is unique per company. Two issuances with the same API key and same external_ref reference the same document. See Retry and idempotency.
  • control_number is never regenerated. Once assigned, it stays even across retries.
  • Endpoints that accept an id_or_ref accept all three identifiers interchangeably — UUID or your external_ref both work in /status/{id}, /invalidate/{id}, /file/{id}, etc.

If you don't send external_ref, Ocote assigns one automatically derived from the UUID. It works, but you lose the ability to find the document from your side without storing the returned UUID, and you lose the safe-retry window after a network timeout.

Recommendation: generate your own external_ref with a readable convention (SALE-2026-000042, INV-A1B2C3, anything), store it before calling the API, and send it in every request.

HTTP status codes

The API follows standard REST conventions with one important nuance for issuance:

CodeWhenWhat to do
200 OKThe operation was registered in Ocote. Could be success, contingency, or MH rejection.Read the body flags.
400 Bad RequestPayload validation error (missing field, malformed NIT, negative quantity).Fix the payload.
401 UnauthorizedAPI key missing, invalid, or company disabled.Check the key.
404 Not FoundDocument not found by id_or_ref, or nonexistent route.Verify the identifier.
409 ConflictOperation incompatible with current state (e.g. invalidating an already-invalidated document).Query /status/{id} to see current state.
429 Too Many RequestsRate limit exceeded.Wait and respect Retry-After. See Rate limits.
500 Internal Server ErrorUncontrolled error in Ocote.Retry after a few seconds; if it persists, contact Ocote.
200 OK does not always mean "MH accepted"

A 200 response with rejected: true means: Ocote registered your request correctly, but MH rejected the document due to recipient data. Correct and retry with the same external_ref.

See the full flags table in Responses and states.

Encoding and content-type

  • Request: every POST must carry Content-Type: application/json and a UTF-8 encoded body.
  • Response: UTF-8 JSON. Accents and Spanish characters do not require escaping.
  • Downloaded files: ticket in application/pdf, JSON in application/json (with the full JWS including MH stamp).

Dates and time zones

  • All dates in responses are ISO 8601 with time zone. Ocote runs in America/El_Salvador (UTC-6). Example: 2026-04-21T10:42:15-06:00.
  • For date filters in listing/query endpoints, use YYYY-MM-DD format (e.g. date_from=2026-04-01). Interpreted in local time (UTC-6).
  • You do not send dates in issuance requests. Ocote assigns the posting timestamp at the moment it accepts the request.

Amounts, currency, and decimals

  • Single currency: USD. The API does not accept or return amounts in any other currency. El Salvador operates in US dollars as legal tender for DTE.
  • All unit prices are sent VAT-included for local sales (types 01, 03, 14). Ocote internally breaks down taxable base and 13% VAT.
  • Decimals: accepts up to 2 decimals in amounts and up to 8 decimals in quantities (for fractional weight/volume units). Amounts are rounded to 2 decimals in the final DTE per MH rules.
  • Do not send currency symbols or thousand separators. Correct: 100.00. Incorrect: "$100", "1,000.00".

Enumerated values that aren't self-describing

Some MH fields use numeric codes that can't be inferred from context. The most frequent:

FieldValuesMeaning
doc_type"01", "03", "05", "11", "14"Invoice, CCF, Credit Note, Export Invoice, Excluded Subject.
type_contributor1, 2, 3, 4Small, Medium, Large, Government.
transaction_condition1, 2, 3Cash, Credit, Other.
payment_method"01", "02", "03", "05", "08", …Cash, Debit Card, Credit Card, Transfer, Electronic Money. See MH catalogs.

Optional fields vs omitted fields

The API distinguishes between absent field (you didn't send it) and field sent with empty value. Practical rule:

  • If a field is optional and you don't need it, omit it. Don't send "email": "" or "email": null if you don't want an email; just don't include the key.
  • Explicit null values are accepted as equivalent to "omitted" in most fields, but some MH validators reject them. Prefer omitting.

Known bug: phone

The customer.phone field is currently not mapped correctly to the internal model and may trigger a validation error if sent. Workaround: omit the phone field from the customer object until the fix is deployed.

This bug does not affect issuance of any document, only the mapping of the recipient phone to the customer record in Ocote. MH does not require phone in any DTE type.

Versioning

The API is in its first public stable version. There is no version prefix in the URL (/api/connect/... not /api/connect/v1/...). Future changes follow these rules:

  • Additive changes don't break. Adding new response fields or optional request parameters will never require client changes.
  • Breaking changes are announced in advance and introduced under a dedicated /api/connect/v2/ path, keeping v1 during a deprecation period.

See Changelog for the change history.