For developers

API and webhooks

Run the same deterministic LC check from your own system: the same extraction, the same UCP 600 and ISBP 821 rules, the same result as in the browser. API access is part of the Enterprise plan.

What you can do

A deliberately small, versioned surface under /api/v1. A partner check goes through exactly the same extraction, redaction and rule engine as a check in the browser.

MethodPathScopeWhat it does
GET/api/v1/pingany active keyConfirms your key works. Returns the account email and plan. Uses no check.
GET/api/v1/usageany active keyPlan, included checks per period, checks used this period, remaining checks and period end. Uses no check.
POST/api/v1/lc-checkslc:writeRuns a full LC Clear check on the documents you upload and returns the result. Billed exactly like a check in the browser.
GET/api/v1/lc-checks/{id}lc:readFetches a past result again. Only checks belonging to the key's own account.
POST/api/v1/lc-cases/{case_id}/document-checkslc:writeERP pre-check: checks the invoice (optionally with the bill of lading and the sales order) against the credit saved with one of your cases, without requiring the transport document. Billed like a check.
POST/api/v1/lc-reviewslc:writeCredit Review: reviews the credit itself before any document exists, as seller (side=exporter) or as buyer (side=importer). Uses no check.
GET/api/v1/lc-caseslc:readThe cases of your account, each with whether a credit is saved to it: how you find the case_id. Uses no check.
GET/api/v1/finding-codesany active keyCatalogue of every explanation_code an LC Clear check can return, with its params, possible severities, the date it was added and, for a retired code, the date it was retired, plus the current rule set version. Uses no check.

Authentication

The API authenticates with API keys, not a username and password, so your integration does not break when someone changes their password.

  • You create keys yourself under Settings in your DocAccord account (verified email address required).
  • Every request sends the key as a header: Authorization: Bearer da_live_.... Browser session logins are not accepted on /api/v1.
  • The key is shown exactly once. Only a SHA-256 hash and a short prefix for recognition are stored. A lost key is replaced, not recovered.
  • Scopes: lc:read fetches results, lc:write runs billable checks. New keys get both; you can narrow a key to one scope in Settings, for example for a system that only collects results.
  • Choose the lifetime: 30, 90, 180, 365 (default) or 730 days. Every new key expires. Up to 10 active keys per account. A revoked key stops working immediately. The owner and admins of the organisation see and revoke every key of the organisation.
  • Rotating without a gap: create the new key and name the one it replaces (replaces, optional overlap_hours from 1 to 72, default 24). The old key then expires after that period instead of being revoked at once.
  • The Enterprise plan is checked on every call, not only at creation. If an account leaves the Enterprise plan, its keys answer with 403.

Your first call

Start with ping. It resolves your key, runs no check and costs nothing.

curl
curl https://api.docaccord.com/api/v1/ping \
  -H "Authorization: Bearer da_live_YOUR_KEY"
Response 200
{
  "ok": true,
  "account_email": "trade-ops@example.com",
  "plan": "ENTERPRISE"
}

If the key is missing, unknown, revoked or expired, the API answers with 401.

Run a check

One multipart/form-data request with your documents. The response comes back synchronously once the check is stored.

  • Required: the credit and the commercial invoice. Each document either as a file (lc_file, invoice_file) or as text (lc_text, invoice_text), never both.
  • Transport documents, same pattern (_file or _text): bill_of_lading, multimodal_transport_doc, air_transport_doc, road_rail_inland_waterway_doc, charter_party_bill_of_lading, sea_waybill, courier_receipt. At least one is required unless field 46A of the credit asks for none; that is checked once the credit is read, before anything is billed.
  • Further transport sets (for example a second bill of lading, or an air waybill next to the bill of lading): up to three files in additional_transport_files, one set per file, with its type at the same position in additional_transport_types (such as BILL_OF_LADING, AIR_TRANSPORT_DOC). A check stays one credit.
  • Further inspection certificates (for example a certificate of quality and a certificate of quantity, as a commodity credit asks for them separately): the first certificate stays in inspection_certificate_file or inspection_certificate_text, up to three more go as files in additional_inspection_certificate_files, one certificate per file; each is examined in full (issuer, signature, timing, link to the goods). Without the first certificate the API answers 400. A check stays one credit. In the result the further certificates appear as additional_inspection_certificates[n] in provenance.
  • Optional, same pattern: packing_list, weight_list, certificate_of_origin, insurance_certificate, bill_of_exchange, beneficiary_certificate, inspection_certificate. Plus the shipment advice as text in shipment_advice_text (a file variant follows once the size limit at the entry point is adjusted), and a guarantee the credit asks for as a document as text in guarantee_text.
  • Since 1.10.0, all optional: transfer_substitution (a JSON object: second_beneficiary_presentation_date, substitution_date, invoice_substituted, draft_substituted) when the first beneficiary of a transferred credit substitutes its own invoice (UCP 600 Art. 38(h)); document_additions (a JSON object per document slot with present, authenticated, supplies_required_data) for data added by hand or on the reverse.
  • Files up to 20 MB: PDF, scans and photos (PNG, JPEG, GIF, WEBP, TIFF, BMP), DOCX, XLSX and text formats. The format is detected from the file content, not its name. The invoice may also be an EN 16931 e-invoice (XRechnung, ZUGFeRD, Factur-X, UBL or CII): as an XML file, or as a PDF with the XML attached. An XML file is read without AI (reading route E_INVOICE_XML in provenance); for a PDF with XML attached the pages are read and the XML then replaces the numbers, dates and ids. The bill of lading may also be an electronic bill of lading as a DCSA eBL 3.0 transport document (a JSON file, bill_of_lading_file or bill_of_lading_text): it is read from its data without AI (reading route EBL_DCSA_JSON) and counts as an electronic record where the credit is subject to eUCP. Only the data is read: the authenticity of the record, the platform and the title holder are not checked.
  • strictness: STANDARD (default) or STRICT.
  • amount_to_be_drawn (optional): the amount you claim under the credit, in its currency. When the invoice is above the credit, this amount decides (UCP 600 Art. 18(b)); without it and without a draft you get a warning to confirm.
  • presentation_date (optional, YYYY-MM-DD): the day you will present the documents. With it the API checks the presentation period (UCP 600 Art. 14(c)) and that no document is dated later (Art. 14(i)); without it a note (presentationDateNotGiven) says neither was checked.
  • instalment_number and earlier_instalments_on_time (optional): under a credit with an instalment schedule, which instalment you present (1 for the first) and whether every earlier instalment was shipped and drawn in its period (true or false). Without them the instalment is inferred from the shipment date (UCP 600 Art. 32).
  • presented_counts (optional): a JSON object with the originals and copies you present per document, for example {"insurance_certificate": {"originals": 1, "copies": 0}} (the keys are the document fields of the request). With it the check tells you when the credit or UCP 600 asks for more (Art. 17, Art. 19 to 22 and 28(b)); without it you keep the notes that the count cannot be verified. earlier_drawn_amount, earlier_drawn_quantity and earlier_containers_shipped (optional): what earlier drawings under this credit took, in its currency and unit. With them the API checks the credit as a whole (total amount, total quantity, the last containers; UCP 600 Art. 18(b), 30 and 31). An invalid value is a 400 before anything is read or billed.
  • earlier_shipments_count and carry_over (optional): for a credit you draw in several shipments. earlier_shipments_count is how many shipments were drawn under the credit before this one; under a credit that prohibits partial shipments (field 43P) one earlier shipment gives a warning (secondDrawingPartialsProhibited, UCP 600 Art. 31(a)). With carry_over set to true the API fills every earlier-drawings input you leave out (earlier_drawn_amount, earlier_drawn_quantity, earlier_shipments_count, instalment_number) from your earlier checks of the same credit reference; what you send always wins. Each shipment is a new check and uses one check credit.
  • revolution_number and earlier_drawn_amount_this_revolution (optional, since 1.11.0): for a revolving credit. revolution_number is the revolution this presentation falls in (1 or higher), earlier_drawn_amount_this_revolution what that revolution has already drawn, in the currency of the credit. Without them a drawing above the amount of one revolution is a warning to confirm (revolvingPerRevolutionLimit), never a discrepancy of the credit: only the aggregate for all revolutions is a fixed limit (UCP 600 Art. 18(b) and the revolving clause of the credit, field 47A). With them the API judges the revolution itself (revolvingRevolutionExceeded), and a revolution number that does not fit the other figures stays a warning.
  • invoice_shown (optional): a JSON object with the voyage facts the commercial invoice shows and a commodity credit may ask it to show, for example {"nor_date": "2026-03-14", "cod_date": "2026-03-17", "bl_date": "2026-03-10", "bl_quantity": 29712.345, "bl_quantity_unit": "MT", "bl_quantity_basis": "AIR"}. Keys: nor_date, cod_date, bl_date (YYYY-MM-DD), bl_quantity, bl_quantity_unit, bl_quantity_basis (AIR or VACUUM), bl_quantity_weight_kind (NET or GROSS), vessel_name, port_of_loading. A fact you leave out is read as “the invoice does not show it”; what you state wins over what the API reads from the invoice itself. Without the field the API uses the voyage facts it reads from the invoice (NOR date, COD date, B/L date, B/L quantity with its basis in air or in vacuum, vessel, port of loading); only when they cannot be read does a note (invoiceContentNotExamined) say they were not examined. An invalid value is a 400 before anything is read or billed.
  • presentation_medium (optional): PAPER, ELECTRONIC or MIXED, how the documents go to the bank, with MIXED plus electronic_doc_types (comma separated, for example INVOICE,PACKING_LIST). It matters only when the credit is subject to eUCP, which the API reads from fields 40E, 46A and 47A.
  • notice_of_completeness_date (optional, since 1.11.0, a date): the day the bank received your notice of completeness. For an electronic presentation under an eUCP credit the presentation is complete only when everything, this notice included, has reached the bank (eUCP 2.1 Art. e6(c)), so the API judges the expiry (UCP 600 Art. 6(e)) and the presentation period (Art. 14(c)) on the later of this day and presentation_date. When the notice is the later day it says so in the note eucpNoticeDateGoverns; when the notice is earlier than presentation_date, that date stays and the warning eucpNoticeBeforePresentationDate names both days. For a paper presentation, or a credit not subject to eUCP, presentation_date stays the date that counts.
  • amendment_file or amendment_text (optional): one amendment (an MT707 or an amendment letter from the bank) you have not answered yet, with amendment_status PENDING, ACCEPTED or REJECTED (PENDING when left out). The documents are then checked against the credit as issued and the amended credit (UCP 600 Art. 10(c)); amendment_benchmark in the result says which one the findings are measured against. A status without an amendment is a 400.
  • If a required document is missing, the API answers with 400 before any document is processed or billed.
  • The credit is read first, then every other document at the same time, not one after the other. The response comes as soon as the slowest document has been read.
curl
curl -X POST https://api.docaccord.com/api/v1/lc-checks \
  -H "Authorization: Bearer da_live_YOUR_KEY" \
  -H "Idempotency-Key: shipment-4711-presentation-1" \
  -F "lc_file=@lc.pdf" \
  -F "invoice_file=@invoice.pdf" \
  -F "bill_of_lading_file=@bill_of_lading.pdf" \
  -F "packing_list_file=@packing_list.pdf" \
  -F "strictness=STANDARD"
Response 200 (abbreviated)
{
  "id": 1842,
  "report_number": "LCC-2026-001842",
  "lc_reference": "LC-2026-0457",
  "overall_status": "REJECTED_DISCREPANCIES",
  "compliance_score": 85,
  "findings": [
    {
      "rule_reference": "UCP 600 Art. 18(c) / ISBP 821 C3",
      "severity": "CRITICAL_ERROR",
      "affected_document": "Commercial Invoice #INV-2291",
      "field_name": "Goods Description",
      "found_value": "100% cotton men's shirts",
      "expected_value": "100% cotton men's shirts, style X",
      "explanation_code": "goodsDescExactMismatch",
      "explanation_params": {},
      "requires_acknowledgment": false,
      "suggested_fix_code": "goodsDescExactMismatchFix",
      "suggested_fix_params": { "lcText": "100% cotton men's shirts, style X" },
      "affected_doc_type": "INVOICE"
    }
  ],
  "created_at": "2026-09-23T09:14:02.118204"
}
  • overall_status is PASSED_FOR_BANK, WARNINGS_FOUND, DISCREPANCIES_TO_CONFIRM or REJECTED_DISCREPANCIES. DISCREPANCIES_TO_CONFIRM means no finding is a CRITICAL_ERROR yet, but at least one WARNING would be one if the reading is right (its stated_severity is then CRITICAL_ERROR). If the reading is right, this is a discrepancy a bank can refuse on: the person checks it against the document and confirms the reading, and the finding then becomes a CRITICAL_ERROR. severity is CRITICAL_ERROR, WARNING or INFO.
  • Each finding names the rule reference, the affected document and field, and the found and expected values. Instead of a rendered sentence the API returns a stable explanation_code with parameters, so you can render the text in your own system.
  • requires_acknowledgment is true when a person must confirm the finding because the data alone cannot decide it. suggested_fix_code with suggested_fix_params is the suggested correction, affected_doc_type the document type. Since 1.11.0, all optional and null where they do not apply: stated_severity (CRITICAL_ERROR on a WARNING the check lowered because its reading is not confirmed yet) and, set only together with it, gated_fields (the fields of the reading the finding rests on) and gated_doc_type (the document type of that reading). Since 1.12.0, the four fields remedy (FACT, CORRECTABLE, READING, CREDIT, NOTE or CUSTOMS: what a person can still do about the finding), routes (for a FACT finding, the ways out that are left: AMENDMENT or EXTENSION, WAIVER, APPROVAL_BASIS, COLLECTION; empty otherwise), copyable and copy_text (set only when the expected value is the wording of the credit itself, to paste, never for a date, a figure or a FACT finding), and the catalogue GET /api/v1/finding-codes carries remedy per code. The params of a published code never change; a new meaning gets a new code, and the old one stays in the GET /api/v1/finding-codes catalogue for 90 days.
  • provenance says what the check ran on: ruleset_version (the date of the rule set) and, per document, whether the DocAccord Reader read it (extraction_model, value docaccord-reader), whether an independent second reading ran (verify_model, value docaccord-reader), the reader version (prompt_sha) and the reading route (extraction_route). Both fields are empty when a document was typed rather than read. An old result stays explainable after the rules or the reader changed.
  • Use the id to fetch the result again later via GET /api/v1/lc-checks/{id}.

Retries without double billing

  • Optional header Idempotency-Key. If the same request is repeated with the same key, you get the stored result back: no second check, no second charge.
  • The same key with a different request returns 409. So does a second request while the first one with that key is still running.
  • For uploaded files, the request is recognised by the content of the file (SHA-256), not its name. Use a new key for every new document set.
  • Stored results are kept for at least 24 hours.

ERP pre-check

Check an invoice against the credit saved with a case at the moment your ERP creates it. Without the bill of lading, the packing list or the other documents: they come later. The result says what the bank would fault on this invoice before it leaves the house. The decision is the bank's.

  • The flow: a colleague reviews the credit in the DocAccord app and chooses Save credit terms to a case. Your ERP then calls POST /api/v1/lc-cases/{case_id}/document-checks for every invoice. Find the case_id with GET /api/v1/lc-cases (narrowed with lc_reference to the credit number).
  • The credit is not sent: the case's latest saved version is used. credit in the result names the case, the version and the save date it was checked against. Without a saved version (or while saving is switched off on the server) the API answers 409 with a note on what to do, before anything is read or billed. A case of another account and a case that does not exist are both 404.
  • The invoice is required (invoice_file or invoice_text, as on a check, including an EN 16931 e-invoice). Optional: the bill of lading (bill_of_lading_file or bill_of_lading_text, including a DCSA eBL 3.0 JSON) and the sales order (order confirmation, proforma invoice, sales contract or purchase order) as order_file or order_text, or as a JSON object in order_json that your ERP builds from its own data and that is compared with no reading. An order_json has at most 50,000 characters. Plus strictness, amount_to_be_drawn and presentation_date as on a check.
  • No transport document is required. Whatever else field 46A of the credit asks for counts as not yet presented and gives no "document missing" finding. Every other rule runs on what you sent. documents_checked names the documents that were checked; overall_status holds for those documents, not for a whole presentation.
  • The sales order is compared with the same saved credit (buyer, seller, goods, quantity, price, amount, currency, trade term, ports, delivery date, partial shipments, payment terms). order_check lists every difference as a warning with the wording you can ask the buyer's bank to amend, and what was not compared and why. It is not part of overall_status. A value the reading marked as unsure is not compared; an order_json counts as your own statement.
  • Billing, scopes, rate limit and Idempotency-Key are those of POST /api/v1/lc-checks: lc:write, one check per request once everything is read, 60 requests an hour per key. The documents are read at the same time. The run counts in your included and bought checks, but does not appear as a whole check in the app's history and gets no certificate. No webhook is sent.
  • The request size limit on this route is the API's general one (21 MiB per request, each file up to 20 MB).

Find the case_id

curl
curl "https://api.docaccord.com/api/v1/lc-cases?lc_reference=LC-2026-0457"   -H "Authorization: Bearer da_live_YOUR_KEY"
Response 200
{
  "storage_enabled": true,
  "cases": [
    {
      "id": 4711,
      "lc_reference": "LC-2026-0457",
      "lc_expiry_date": "2026-11-30",
      "latest_shipment_date": "2026-10-31",
      "credit": {
        "saved": true,
        "version": 2,
        "saved_at": "2026-09-24T08:12:44.101233",
        "source": "credit_review",
        "terms_available": true
      }
    }
  ]
}

Example SAP: billing document released

curl
# Sent by your SAP integration layer (SAP Integration Suite, or an ABAP class
# through an HTTP destination) when the billing document is released.
# 4711 is the DocAccord case id you stored on the sales order.
curl -X POST https://api.docaccord.com/api/v1/lc-cases/4711/document-checks   -H "Authorization: Bearer da_live_YOUR_KEY"   -H "Idempotency-Key: sap-billing-90001234"   -F "invoice_file=@90001234.pdf"   -F "order_json=<sales_order_5000123.json"
sales_order_5000123.json
{
  "document_kind": "OTHER",
  "order_number": "5000123",
  "order_date": "2026-08-14",
  "buyer": { "name": "Rotterdam AgriTrade B.V." },
  "seller": { "name": "Rhine Valley Commodities GmbH" },
  "currency": "EUR",
  "amount": 248000.00,
  "quantity": 49500,
  "quantity_unit": "MT",
  "stated_incoterm": "CIF",
  "incoterm_place": "Rotterdam",
  "delivery_date": "2026-09-05"
}
  • Field names of the standard A_SalesOrder OData service (check them against your own release): SalesOrder becomes order_number, SalesOrderDate becomes order_date, TransactionCurrency becomes currency, TotalNetAmount becomes amount, IncotermsClassification becomes stated_incoterm, IncotermsLocation1 becomes incoterm_place, RequestedDeliveryDate becomes delivery_date. The buyer's name comes from the business partner of SoldToParty, quantity and unit from the items (RequestedQuantity, RequestedQuantityISOUnit).
  • Leave a key out when your system does not know the value: it is not compared, and order_check.not_compared says so. Never send anything that is not on the order.

Example CargoWise: documents on the shipment

curl
# Sent by your integration when the commercial invoice and the bill of lading
# are attached to the shipment (for example through CargoWise's eAdaptor, or a
# workflow rule that calls your middleware). The bill of lading may be the PDF or
# a DCSA eBL 3.0 JSON record.
curl -X POST https://api.docaccord.com/api/v1/lc-cases/4711/document-checks   -H "Authorization: Bearer da_live_YOUR_KEY"   -H "Idempotency-Key: cargowise-S00012345-docs-1"   -F "invoice_file=@commercial_invoice.pdf"   -F "bill_of_lading_file=@ebl.json;type=application/json"   -F "presentation_date=2026-08-20"

Result

Response 200 (abbreviated)
{
  "id": 1843,
  "report_number": "LCP-2026-001843",
  "case_id": 4711,
  "lc_reference": "LC-2026-0457",
  "overall_status": "REJECTED_DISCREPANCIES",
  "documents_checked": ["INVOICE"],
  "credit": {
    "case_id": 4711,
    "case_reference": "LC-2026-0457",
    "version": 2,
    "saved_at": "2026-09-24T08:12:44.101233",
    "source": "credit_review"
  },
  "findings": [
    {
      "rule_reference": "UCP 600 Art. 18(c) / ISBP 821 C3",
      "severity": "CRITICAL_ERROR",
      "affected_document": "Commercial Invoice #INV-2291",
      "field_name": "Goods Description",
      "found_value": "100% cotton men's shirts",
      "expected_value": "100% cotton men's shirts, style X",
      "explanation_code": "goodsDescExactMismatch",
      "explanation_params": {},
      "requires_acknowledgment": false,
      "suggested_fix_code": "goodsDescExactMismatchFix",
      "suggested_fix_params": { "lcText": "100% cotton men's shirts, style X" },
      "affected_doc_type": "INVOICE"
    }
  ],
  "order_check": {
    "findings": [],
    "compared": ["buyer", "seller", "currency", "amount", "quantity", "incoterm", "delivery_date"],
    "not_compared": [{ "key": "unit_price", "reason": "not_on_order" }],
    "order_kind": "OTHER"
  },
  "created_at": "2026-09-29T09:14:02.118204"
}

The findings have the same shape as on a check (explanation_code with params, suggested_fix_code). The report number starts with LCP instead of LCC.

Credit Review

Review the credit itself the moment it arrives or while it is still a draft, when an amendment is still a phone call. The exporter gets the Credit Review as seller, the importer the Credit Review as buyer: one route, two views.

  • The credit is required as lc_file (PDF, scan, photo) or lc_text (the pasted MT700 with its MT701s), never both. side is exporter (the default, the beneficiary's view, the seller's review) or importer (the applicant's view, the buyer's review). is_draft=true marks a draft the parties are still negotiating: the same checks run, but a finding is then a negotiating position rather than an amendment.
  • Optional: a sales order as order_file, order_text or order_json (the same keys as on the ERP pre-check). order_check lists every difference from the credit as a warning, apart from the status, counts and score.
  • Optional: case_id (one of your cases from GET /api/v1/lc-cases) or save_to_case=true saves the terms as a version with the source credit_review. Without a case_id the case is found or opened by the credit's reference number. The terms are saved only when every field the reading doubted was confirmed by a person. The API cannot do that, so for a reading with a doubt credit_saved.status is unconfirmed and nothing is saved. A draft is never saved (422). Without a save request the route saves nothing and credit_saved is null.
  • The response gives overall_status (WORKABLE, REVIEW_ADVISED, BLOCKED, or TO_COMPLETE for a draft with nothing but blanks left open), risk_band and risk_score (the commercial risk of the terms, our own weighted count, not a probability and not a rating by any bank), plus placeholder_count and placeholder_fields (the blanks of a draft), readiness (how ready the text is to be sent) and risk_breakdown (what the score is made of), the findings with explanation_code, params and the severity for the other party, and advice and commercial, the commercial reading. The wording in suggested_clause is English because it goes into a credit. When it is locked for your account after the one free reveal, wording is null and locked is true; the finding itself is never withheld. A generic note that a stronger finding on the same clause replaces stays in the list marked superseded true: superseded_by is the code of the replacing finding and superseded_by_id its id in this response (null when the finding is not superseded), and the replacing finding lists the ids it replaced in merged. Show, count and score only the findings that are not superseded; finding_counts gives total, shown, superseded and to_complete. All of these are additive fields.
  • Billing: no check, as in the app. The reading goes through the same gate as POST /api/extract and the reading allowance for credits. A pasted MT700 is read too (the model reads it, a deterministic SWIFT reader reads the same text a second time and settles every difference); the review, the comparison with the order and the wording need no model. Scopes (lc:write), 60 requests an hour, the Idempotency-Key (its own scope) and the error format are those of POST /api/v1/lc-checks. The route is stateless: no id, no fetch route, no webhook. A retry with the same key returns the stored answer without reading again.
curl
# The review we run on an arriving credit (side=exporter, the seller's view).
# Use side=importer for the buyer's review of a draft you are about to
# send to your bank. lc_text takes the pasted MT700 instead of a file.
curl -X POST https://api.docaccord.com/api/v1/lc-reviews   -H "Authorization: Bearer da_live_YOUR_KEY"   -H "Idempotency-Key: credit-LC-2026-0457-review-1"   -F "lc_file=@lc.pdf"   -F "side=exporter"   -F "is_draft=false"
Response 200 (abbreviated)
{
  "lc_reference": "LC-2026-0457",
  "side": "exporter",
  "is_draft": false,
  "overall_status": "BLOCKED",
  "error_count": 1,
  "warning_count": 1,
  "risk_band": "MEDIUM",
  "risk_score": 33,
  "findings": [
    {
      "id": "IR-001",
      "rule_reference": "L/C Field 31D",
      "severity": "CRITICAL_ERROR",
      "field_name": "Expiry date (31D)",
      "explanation_code": "irAlreadyExpired",
      "explanation_params": { "expiry": "2026-09-01" },
      "requires_acknowledgment": false,
      "counterparty_severity": "CRITICAL_ERROR",
      "text_variant": null,
      "suggested_clause": null
    },
    {
      "id": "IR-003",
      "rule_reference": "UCP 600 Art. 28(g)",
      "severity": "INFO",
      "field_name": "Documents required (46A)",
      "explanation_code": "irInsuranceRisksUnstated",
      "explanation_params": { "document": "Insurance Certificate" },
      "requires_acknowledgment": false,
      "counterparty_severity": "INFO",
      "text_variant": null,
      "suggested_clause": {
        "clause_id": "LC-INS-02",
        "wording": "The insurance line in field 46A is amended to state the risks to be covered, ...",
        "locked": false,
        "rule_reference": "UCP 600 Art. 28(g)",
        "guidance_code": "clause.insuranceRisks"
      }
    }
  ],
  "advice": [
    { "code": "thirdPartyDocuments", "tone": "COST", "params": { "count": 2, "list": "Certificate of Origin; Insurance Certificate" }, "cost": 300.0, "cost_currency": "EUR" }
  ],
  "commercial": null,
  "order_check": null,
  "credit_saved": null,
  "ruleset_version": "2026-09-29",
  "created_at": "2026-10-01T09:14:02.118204+00:00"
}

Webhooks

Instead of polling for a finished check, your system receives a signed notification. Delivery runs after the API response is sent and never delays it.

  • check.completed: a check has been stored. data carries the same result as the POST /api/v1/lc-checks response. The ERP pre-check sends no webhook: its response is the result.
  • check.failed: a check could not be completed. data carries lc_reference and reason. Any check charged for it is refunded.
  • https URLs only, on a host that resolves to public internet addresses only (private, loopback and link-local addresses are refused, at registration and before every delivery). Up to 5 active endpoints per account, each subscribed to one or both events.
  • Endpoints are registered by a signed-in user of your account, not with an API key, so a leaked key cannot add new delivery targets.
  • On registration you receive a signing secret (whsec_...). It is shown once.
Delivery (abbreviated)
POST /your/webhook/path
Content-Type: application/json
X-DocAccord-Event: check.completed
X-DocAccord-Timestamp: 1790154842
X-DocAccord-Signature: sha256=5f0c1d...

{"event":"check.completed","data":{"id":1842,"report_number":"LCC-2026-001842",...}}

Verify the signature

  • Signature = HMAC-SHA256 with your secret over <timestamp>.<raw body>, sent as X-DocAccord-Signature: sha256=<hex> together with X-DocAccord-Timestamp and X-DocAccord-Event.
  • The timestamp is part of the signed material. Reject deliveries outside your own tolerance so a captured request cannot be replayed later.
  • Verify against the raw request body bytes before parsing JSON, and compare in constant time.
Python
import hashlib
import hmac
import time

TOLERANCE_SECONDS = 300

def verify(secret: str, headers: dict, raw_body: bytes) -> bool:
    timestamp = headers["X-DocAccord-Timestamp"]
    signature = headers["X-DocAccord-Signature"]
    if abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS:
        return False
    signed = timestamp.encode() + b"." + raw_body
    expected = "sha256=" + hmac.new(
        secret.encode(), signed, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

Delivery and retries

  • A response with a status below 300 counts as delivered. Timeout per attempt: 10 seconds.
  • On 5xx or network errors, up to 5 attempts with exponential backoff (1, 2, 4, 8 seconds). A 3xx or 4xx response is not retried, and redirects are not followed.
  • Every delivery is logged per endpoint: attempts, last status code, last error (for example timeout, connection failed or HTTP 503).
  • Webhooks are a notification, not a dependency. GET /api/v1/lc-checks/{id} stays the authoritative source if a delivery does not arrive.

Limits and billing

  • API access is included in the Enterprise plan. Other plans cannot create keys, and calls are refused with 403.
  • A check through the API is billed exactly like a check in the browser: first from the period's included checks, then from the checks you bought.
  • When included and bought checks are used up, POST /api/v1/lc-checks answers with 402. GET /api/v1/usage shows where you stand at any time.
  • Enterprise accounts can buy check packs of any quantity on the Billing page and switch on automatic top-up: when the checks left fall below a level you choose, the quantity you chose is bought and charged to your subscription's payment method (Paddle sends the invoice), up to a monthly limit you set. The checks arrive once Paddle confirms the payment, usually within a minute. A 402 while a top-up is running carries Retry-After, and GET /api/v1/usage shows auto_topup_enabled and topup_pending.
  • ping, usage, the list of cases, fetching past results and the Credit Review (POST /api/v1/lc-reviews) use no check. An ERP pre-check (POST /api/v1/lc-cases/{case_id}/document-checks) uses one check once all its documents are read, exactly like POST /api/v1/lc-checks; if reading fails it costs nothing.
  • Rate limit: 60 requests per hour per API key on POST /api/v1/lc-checks, POST /api/v1/lc-reviews and POST /api/v1/lc-cases/{case_id}/document-checks, 200 per hour per key on every other authenticated route. Above that the API answers with 429.
  • One check reads at most 15 documents, not counting the credit and its amendment. A request with more is refused with 429 before the first one is read; contact us if you need a larger set. Documents of requests that fail after they were read count until a check completes, or for at most 24 hours after the first of those readings; a request whose documents no longer fit next to them gets 429 with Retry-After. Parallel requests do not count against each other.
  • Every API key has a daily limit for AI document reading that starts over at midnight UTC. At 80 percent the account's email address gets one warning a day; once the limit is reached the API answers 429 with Retry-After until midnight UTC. Close to the limit, a request can also get 429 with Retry-After 60 while other requests with the key are still running; retry it after a minute. Checks already running are not interrupted. Write to us if you need a higher limit.

Error codes

Errors come back as JSON with a human-readable detail field.

400Required document missing, file and text for the same document, or file not readable
401Key missing, unknown, revoked or expired
402Included checks and bought checks used up
403Account not on the Enterprise plan, the key lacks the scope, or AI document reading is paused for the account (contact us)
404Check or case does not exist or does not belong to your account
409Idempotency-Key reused with a different request, or the first request is still running; on the ERP pre-check also: the case holds no saved credit (or saving credit terms is switched off on the server, or the saved credit is empty)
413File larger than 20 MB, more than 100 pages or more than 30 scanned pages, an image above 100 megapixels, pasted text above 1,000,000 characters, or a scan that took too long to process
422A field fails validation, a page (other than a photo or scan) is larger than A0, or the PDF is password protected; on POST /api/v1/lc-checks also: nothing was read from the credit (no reference, amount, expiry date or 46A), nothing is charged
429Rate limit exceeded, or the key's daily limit for AI document reading reached (until midnight UTC; Retry-After says how long to wait), or more documents than one check reads (15, readings of failed requests included)
503Document reading is unavailable right now (an outage at the AI provider, or paused for a few minutes because of unusually high load); Retry-After says how long to wait

Environments

The prefix tells you at a glance which environment a key belongs to. A key only works in the environment that issued it.

EnvironmentBase URLKey prefix
Livehttps://api.docaccord.comda_live_
Sandboxhttps://api-staging.docaccord.comda_test_

Stability and schema

  • A breaking change to a v1 response is never made in place. It ships as /api/v2, and v1 keeps working.
  • If a v1 endpoint is retired, we give at least 90 days' notice: a Sunset header (RFC 8594) on that endpoint, and a direct notice to every partner whose active key used it in the preceding 30 days.
  • The partner API's OpenAPI schema is published at https://api.docaccord.com/api/v1/openapi.json, ready to import into Postman or any OpenAPI tool.

Put the check inside your own system

API access and webhooks are part of the Enterprise plan. Talk to us about your volume and your integration.