Templates & customization

Templates bundle document requirements, checks, and data fields. Discover the builtin catalog, duplicate and customize templates, and author your own checks and data fields.

Every submission is created from a template — the bundle that decides which documents are requested (as document slots), which checks the evaluation executes, and which data fields it extracts. Reading templates tells you exactly what a submission will look like before you create it; authoring them tailors the whole flow to your product.

Builtin vs custom

Every catalog resource — templates, document types, checks, data fields — comes in two flavors:

  • Builtin entries ship with AirCredit. They are identified by a stable string key (e.g. the template default_personal_real_estate_financing, the document type grundbuchauszug, the check payslip.base_salary_consistency), are read-only, and carry no prompt text — how they are evaluated is internal to AirCredit.
  • Custom entries are yours. They are identified by a uuid, carry the content you author (including prompts for checks and data fields), and are private to your API client's organization.

Both flavors appear in the same list endpoints, distinguishable by the builtin flag, and both are referenced the same way — anywhere an id is expected, a builtin key and a custom uuid are equally valid.

Discovering templates

list_templates returns every template you can create submissions from; get_template resolves the full bundle — the document requirements a submission will materialize as slots, the checks, and the data fields. Use the template's id as template_id in create_submission.

The builtin catalogs are enumerated in the API reference and in the generated SDKs as string enums, so you can match on keys in code. They are open enums: new keys appear over time, so always keep a fallback arm for keys your SDK version does not know yet.

Customizing a template

Builtin templates cannot be edited — duplicate one instead, then shape the copy. The three item lists (document requirements, checks, data fields) are each replaced as a whole; array order is display order.

# 1. Copy the builtin template.
curl -X POST https://api.aircredit.de/partner/v1/templates/default_personal_real_estate_financing/duplicate \
  -H "Authorization: Bearer $AIRCREDIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "display_name": "Real estate — broker flow" }'

# 2. Replace its document requirements (use the copy's id and revision from step 1).
curl -X PUT https://api.aircredit.de/partner/v1/templates/$TEMPLATE_ID/document_requirements \
  -H "Authorization: Bearer $AIRCREDIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "expected_revision": 1,
    "items": [
      { "document_type_id": "gehaltsabrechnung", "required": true, "allow_multiple_files": true },
      { "document_type_id": "grundbuchauszug", "required": true },
      { "document_type_id": "expose", "required": false }
    ]
  }'
const copy = await client.duplicateTemplate({
  templateId: "default_personal_real_estate_financing",
  displayName: "Real estate — broker flow",
});

const updated = await client.replaceTemplateDocumentRequirements({
  templateId: copy.id,
  expectedRevision: copy.revision!,
  items: [
    { documentTypeId: "gehaltsabrechnung", required: true, allowMultipleFiles: true },
    { documentTypeId: "grundbuchauszug", required: true },
    { documentTypeId: "expose", required: false },
  ],
});
console.log(updated.documentRequirements.map(r => r.label));
copy = client.duplicate_template(
    template_id="default_personal_real_estate_financing",
    display_name="Real estate — broker flow",
)

updated = client.replace_template_document_requirements(
    template_id=copy.id,
    expected_revision=copy.revision,
    items=[
        {"document_type_id": "gehaltsabrechnung", "required": True, "allow_multiple_files": True},
        {"document_type_id": "grundbuchauszug", "required": True},
        {"document_type_id": "expose", "required": False},
    ],
)
print([r.label for r in updated.document_requirements])
let copy = client.duplicate_template()
    .template_id("default_personal_real_estate_financing")
    .body_map(|b| b.display_name(Some("Real estate — broker flow".into())))
    .send().await?.into_inner();

let updated = client.replace_template_document_requirements()
    .template_id(&copy.id)
    .body_map(|b| b
        .expected_revision(copy.revision.unwrap())
        .items(vec![
            TemplateDocumentRequirementInput::builder()
                .document_type_id("gehaltsabrechnung").required(true).allow_multiple_files(true),
            TemplateDocumentRequirementInput::builder()
                .document_type_id("grundbuchauszug").required(true),
            TemplateDocumentRequirementInput::builder()
                .document_type_id("expose").required(false),
        ]))
    .send().await?.into_inner();

Template writes are guarded by optimistic concurrency: pass the revision you last read as expected_revision. If someone else changed the template in between, the write fails with 409 revision_mismatch — re-read, reapply, retry.

Note

Template edits affect future submissions only. A submission materializes an immutable snapshot of its template at create time, so changing a template never alters submissions that are already in flight — no re-consent, no shifting requirements under an applicant.

Custom document types

When the builtin document-type catalog lacks a paper your flow needs, create a custom document type and reference it from template document requirements like any builtin key. For one-off, case-specific paperwork you can also pass extra_document_requirements directly in create_submission — those become additional slots on that submission only, without touching any template.

Custom checks

A custom check is a prompt you author: the instruction AirCredit's evaluation executes against the submission's documents. Write it like a precise instruction to a diligent reviewer — name the documents involved, state the criterion, and say what should count as a failure.

# 1. Author the check.
curl -X POST https://api.aircredit.de/partner/v1/checks \
  -H "Authorization: Bearer $AIRCREDIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Employer on our partner list",
    "prompt": "Check whether the employer named on the payslip appears on the list of cooperating employers provided in the self-declaration. Pass if it matches exactly or is an obvious subsidiary; flag as open if the employer cannot be identified.",
    "category": "other"
  }'

# 2. Append it to the template's check list (full replacement, existing items included).
curl -X PUT https://api.aircredit.de/partner/v1/templates/$TEMPLATE_ID/check_items \
  -H "Authorization: Bearer $AIRCREDIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "expected_revision": 2,
    "items": [
      { "check_id": "payslip.base_salary_consistency" },
      { "check_id": "bank.salary_matches_payslip" },
      { "check_id": "'$CHECK_ID'" }
    ]
  }'
const check = await client.createCheck({
  title: "Employer on our partner list",
  prompt:
    "Check whether the employer named on the payslip appears on the list of " +
    "cooperating employers provided in the self-declaration. Pass if it matches " +
    "exactly or is an obvious subsidiary; flag as open if the employer cannot be identified.",
  category: "other",
});

const template = await client.getTemplate({ templateId });
await client.replaceTemplateCheckItems({
  templateId,
  expectedRevision: template.revision!,
  items: [...template.checkItems.map(i => ({ checkId: i.checkId })), { checkId: check.id }],
});
check = client.create_check(
    title="Employer on our partner list",
    prompt=(
        "Check whether the employer named on the payslip appears on the list of "
        "cooperating employers provided in the self-declaration. Pass if it matches "
        "exactly or is an obvious subsidiary; flag as open if the employer cannot be identified."
    ),
    category="other",
)

template = client.get_template(template_id=template_id)
client.replace_template_check_items(
    template_id=template_id,
    expected_revision=template.revision,
    items=[*({"check_id": i.check_id} for i in template.check_items), {"check_id": check.id}],
)
let check = client.create_check()
    .body_map(|b| b
        .title("Employer on our partner list")
        .prompt("Check whether the employer named on the payslip appears on the list of \
                 cooperating employers provided in the self-declaration. Pass if it matches \
                 exactly or is an obvious subsidiary; flag as open if the employer cannot be identified.")
        .category(CheckCategory::Other))
    .send().await?.into_inner();

let template = client.get_template().template_id(&template_id).send().await?.into_inner();
let mut items: Vec<_> = template.check_items.iter()
    .map(|item| TemplateCheckItemInput::builder().check_id(&item.check_id))
    .collect();
items.push(TemplateCheckItemInput::builder().check_id(&check.id));

client.replace_template_check_items()
    .template_id(&template_id)
    .body_map(|b| b.expected_revision(template.revision.unwrap()).items(items))
    .send().await?;

Rules of thumb for check prompts:

  • One criterion per check. "Salary on the payslip matches the bank statement" is one check; "…and the tax class is plausible" is a second one. Small checks produce clean, per-criterion verdicts in the result.
  • State the pass condition explicitly. The verdict vocabulary is fixed (fulfilled / open / critical / not_applicable / failed); your prompt decides what lands where, so say which findings are merely open and which are deal-breakers.
  • Name the evidence. Prompts that say where to look ("on the payslip", "in the land register extract") produce results with precise citations.

Editing a custom check is versioned: the id stays stable, future submissions use the new wording, and past evaluation results keep the version they were produced with.

Custom data fields

A custom data field extracts one value from the documents. Alongside the extraction prompt, give it a value_format (currency, years, percent, square_meters, date) so extracted values come back in a predictable unit and shape; fields without a format extract free text. Extracted values appear in the evaluation result with a confidence score and citations — see Evaluations.

Archiving

Deleting a custom catalog entry or template archives it: it disappears from listings and can no longer be referenced going forward, but templates, submissions, and results that already reference it keep working. Archiving is how the catalog stays append-only from your integration's point of view — ids you have stored never dangle.

On this page