> ## Documentation Index
> Fetch the complete documentation index at: https://docs.paywise.de/llms.txt
> Use this file to discover all available pages before exploring further.

# API-only onboarding

> Create a complete company aggregate without interrupting the Partner UX while preserving terms and named-Partner confirmation.

## Outcome

You have one Partner-managed company UUID, the exact UUID of every inline
membership, a Partner lifecycle webhook whose one-time secret is already in a
restricted secret sink, and the company's current readiness projection.

## Lifecycle context

Creation commits the company aggregate, inline memberships, provisional
authorization, events, and idempotent response together. Provisional access is
not confirmed consent: the invited admin later confirms the current
authorization version. Readiness is independent. A draft Case may be created
while readiness is false, but finalization re-checks readiness atomically.

Every inline user receives an invitation. The fresh `201` response returns
each new inline membership's `setup_url` once; reads and an idempotent replay
omit it. The link leads to password setup only for an account this request
created. Every account that existed before receives a sign-in link instead.
An invitee who lost a password-setup link and never used the account can set a
first password through **Passwort vergessen** in the paywise portal; the reset
link goes only to their own address. See
[Manage memberships](/api-docs/partner-api/workflows/manage-memberships).

Use the sandbox host (`https://api-sandbox.paywise.de`) and a Partner key.
Give each logical write a stable named key. An ambiguous timeout is recovered
only by replaying the same body and key within a bounded attempt budget.

## 1. Prove the sandbox before creating the webhook

Every Partner write in this workflow is immediately preceded by this
authenticated information request. Require the case-insensitive
`X-Paywise-Environment` response header to equal `sandbox`.

<CodeGroup>
  ```python Python theme={null}
  import requests

  PAYWISE_API_URL = "https://api-sandbox.paywise.de"
  PAYWISE_PARTNER_KEY = "pw_sbx_your_partner_key"

  response = requests.get(
      f"{PAYWISE_API_URL}/partner/v2/info/",
      headers={"Authorization": f"Bearer {PAYWISE_PARTNER_KEY}"},
      timeout=(5, 30),
  )
  if response.status_code != 200:
      response.raise_for_status()
      raise RuntimeError(f"Expected 200, received {response.status_code}")
  environment = next((value for name, value in response.headers.items() if name.lower() == "x-paywise-environment"), None)
  if environment != "sandbox":
      raise RuntimeError("Refusing Partner write outside the sandbox")
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch(`${process.env.PAYWISE_API_URL.replace(/\/$/, "")}/partner/v2/info/`, {
    method: "GET",
    headers: { Authorization: `Bearer ${process.env.PAYWISE_PARTNER_KEY}` },
    signal: AbortSignal.timeout(30000),
  });
  if (response.status !== 200) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
  const environment = response.headers.get("X-Paywise-Environment");
  if (environment !== "sandbox") throw new Error("Refusing Partner write outside the sandbox");
  ```

  ```java Java theme={null}
  import java.io.IOException;
  import java.net.URI;
  import java.net.http.HttpClient;
  import java.net.http.HttpRequest;
  import java.net.http.HttpResponse;
  import java.time.Duration;

  class ApiOnlyOnboardingRequest {
    public static void main(String[] args) throws IOException, InterruptedException {
      HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
      HttpRequest request = HttpRequest.newBuilder()
          .uri(URI.create(System.getenv("PAYWISE_API_URL") + "/partner/v2/info/"))
          .timeout(Duration.ofSeconds(30))
          .header("Authorization", "Bearer " + System.getenv("PAYWISE_PARTNER_KEY"))
          .GET().build();
      HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
      if (response.statusCode() != 200) throw new IOException(response.body());
      String environment = response.headers().firstValue("X-Paywise-Environment").orElse("");
      if (!"sandbox".equals(environment)) throw new IOException("Refusing Partner write outside the sandbox");
    }
  }
  ```

  ```csharp C# theme={null}
  using System;
  using System.Linq;
  using System.Net.Http;
  using System.Threading;
  using System.Threading.Tasks;

  class ApiOnlyOnboardingRequest
  {
      static async Task Main()
      {
          using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
          using var request = new HttpRequestMessage(HttpMethod.Get,
              $"{Environment.GetEnvironmentVariable("PAYWISE_API_URL")}/partner/v2/info/");
          request.Headers.Authorization = new("Bearer", Environment.GetEnvironmentVariable("PAYWISE_PARTNER_KEY"));
          using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(30));
          using var response = await client.SendAsync(request, cancellation.Token);
          if ((int)response.StatusCode != 200) throw new HttpRequestException(await response.Content.ReadAsStringAsync());
          if (!response.Headers.TryGetValues("X-Paywise-Environment", out var environmentValues)
              || environmentValues.Count() != 1
              || !string.Equals(environmentValues.Single(), "sandbox", StringComparison.Ordinal))
              throw new HttpRequestException("Refusing Partner write outside the sandbox");
      }
  }
  ```

  ```bash cURL theme={null}
  proof="$(curl --fail-with-body --silent --show-error --connect-timeout 5 --max-time 30 \
    --request GET "$PAYWISE_API_URL/partner/v2/info/" \
    --header "Authorization: Bearer $PAYWISE_PARTNER_KEY" \
    --dump-header - --output /dev/null --write-out '\n%{http_code}')"
  status="$(printf '%s\n' "$proof" | tail -n 1)"
  [ "$status" != "200" ] && exit 1
  environment="$(printf '%s\n' "$proof" | awk '
    {
      separator = index($0, ":")
      if (!separator || tolower(substr($0, 1, separator - 1)) != "x-paywise-environment") next
      count++
      value = substr($0, separator + 1)
      sub(/\r$/, "", value)
      sub(/^[ \t]*/, "", value)
      sub(/[ \t]*$/, "", value)
    }
    END { if (count != 1) exit 1; print value }
  ')" || exit 1
  [ "$environment" != "sandbox" ] && exit 1
  :
  ```
</CodeGroup>

## 2. Create the lifecycle subscription and capture its secret once

The response reveals `secret_key` exactly once. Store it directly in a
restricted secret manager or mode-`0600` file; never print it or retain it in
ordinary logs.

<CodeGroup>
  ```python Python theme={null}
  import requests

  PAYWISE_API_URL = "https://api-sandbox.paywise.de"
  PAYWISE_PARTNER_KEY = "pw_sbx_your_partner_key"
  WEBHOOK_COMMAND_ID = "your-webhook-command-id"

  webhook_payload = {
      "url": "https://hooks.example.test/paywise-partner",
      "enabled": True,
      "events": ["company.created", "company.access.confirmed", "company.case_submission_readiness.changed"],
      "description": "Partner onboarding lifecycle",
  }
  response = requests.post(
      f"{PAYWISE_API_URL}/partner/v2/webhooks/",
      headers={
          "Authorization": f"Bearer {PAYWISE_PARTNER_KEY}",
          "Content-Type": "application/json",
          "Idempotency-Key": WEBHOOK_COMMAND_ID,
      },
      json=webhook_payload,
      timeout=(5, 30),
  )
  if response.status_code != 201:
      response.raise_for_status()
      raise RuntimeError(f"Expected 201, received {response.status_code}")
  ```

  ```javascript JavaScript theme={null}
  const webhookPayload = {
    url: "https://hooks.example.test/paywise-partner", enabled: true,
    events: ["company.created", "company.access.confirmed", "company.case_submission_readiness.changed"],
    description: "Partner onboarding lifecycle",
  };
  const response = await fetch(`${process.env.PAYWISE_API_URL.replace(/\/$/, "")}/partner/v2/webhooks/`, {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.PAYWISE_PARTNER_KEY}`, "Content-Type": "application/json", "Idempotency-Key": process.env.WEBHOOK_COMMAND_ID },
    body: JSON.stringify(webhookPayload), signal: AbortSignal.timeout(30000),
  });
  if (response.status !== 201) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
  ```

  ```java Java theme={null}
  import java.io.IOException;
  import java.net.URI;
  import java.net.http.HttpClient;
  import java.net.http.HttpRequest;
  import java.net.http.HttpResponse;
  import java.time.Duration;

  class ApiOnlyOnboardingRequest {
    public static void main(String[] args) throws IOException, InterruptedException {
      String json = "{\"url\":\"https://hooks.example.test/paywise-partner\",\"enabled\":true,\"events\":[\"company.created\",\"company.access.confirmed\",\"company.case_submission_readiness.changed\"],\"description\":\"Partner onboarding lifecycle\"}";
      HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
      HttpRequest request = HttpRequest.newBuilder()
          .uri(URI.create(System.getenv("PAYWISE_API_URL") + "/partner/v2/webhooks/"))
          .timeout(Duration.ofSeconds(30))
          .header("Authorization", "Bearer " + System.getenv("PAYWISE_PARTNER_KEY"))
          .header("Content-Type", "application/json")
          .header("Idempotency-Key", System.getenv("WEBHOOK_COMMAND_ID"))
          .POST(HttpRequest.BodyPublishers.ofString(json)).build();
      HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
      if (response.statusCode() != 201) throw new IOException(response.body());
    }
  }
  ```

  ```csharp C# theme={null}
  using System;
  using System.Net.Http;
  using System.Net.Http.Json;
  using System.Threading;
  using System.Threading.Tasks;

  class ApiOnlyOnboardingRequest
  {
      static async Task Main()
      {
          var payload = new { url = "https://hooks.example.test/paywise-partner", enabled = true, events = new[] { "company.created", "company.access.confirmed", "company.case_submission_readiness.changed" }, description = "Partner onboarding lifecycle" };
          using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
          using var request = new HttpRequestMessage(HttpMethod.Post,
              $"{Environment.GetEnvironmentVariable("PAYWISE_API_URL")}/partner/v2/webhooks/");
          request.Headers.Authorization = new("Bearer", Environment.GetEnvironmentVariable("PAYWISE_PARTNER_KEY"));
          request.Headers.Add("Idempotency-Key", Environment.GetEnvironmentVariable("WEBHOOK_COMMAND_ID"));
          request.Content = JsonContent.Create(payload);
          using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(30));
          using var response = await client.SendAsync(request, cancellation.Token);
          if ((int)response.StatusCode != 201) throw new HttpRequestException(await response.Content.ReadAsStringAsync());
      }
  }
  ```

  ```bash cURL theme={null}
  umask 077
  secret_response="$(mktemp "${TMPDIR:-/tmp}/partner-webhook-created.XXXXXX")"
  chmod 600 "$secret_response"
  trap 'rm -f "$secret_response"' EXIT HUP INT TERM
  status="$(curl --fail-with-body --silent --show-error --connect-timeout 5 --max-time 30 \
    --request POST "$PAYWISE_API_URL/partner/v2/webhooks/" \
    --header "Authorization: Bearer $PAYWISE_PARTNER_KEY" \
    --header "Content-Type: application/json" \
    --header "Idempotency-Key: $WEBHOOK_COMMAND_ID" \
    --output "$secret_response" --write-out '%{http_code}' \
    --data @- <<'JSON'
  {"url":"https://hooks.example.test/paywise-partner","enabled":true,"events":["company.created","company.access.confirmed","company.case_submission_readiness.changed"],"description":"Partner onboarding lifecycle"}
  JSON
  )"
  [ "$status" != "201" ] && exit 1
  ```
</CodeGroup>

## 3. Prove the sandbox again before company creation

<CodeGroup>
  ```python Python theme={null}
  import requests

  PAYWISE_API_URL = "https://api-sandbox.paywise.de"
  PAYWISE_PARTNER_KEY = "pw_sbx_your_partner_key"

  response = requests.get(
      f"{PAYWISE_API_URL}/partner/v2/info/",
      headers={"Authorization": f"Bearer {PAYWISE_PARTNER_KEY}"},
      timeout=(5, 30),
  )
  if response.status_code != 200:
      response.raise_for_status()
      raise RuntimeError(f"Expected 200, received {response.status_code}")
  environment = next((value for name, value in response.headers.items() if name.lower() == "x-paywise-environment"), None)
  if environment != "sandbox":
      raise RuntimeError("Refusing Partner write outside the sandbox")
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch(`${process.env.PAYWISE_API_URL.replace(/\/$/, "")}/partner/v2/info/`, {
    method: "GET", headers: { Authorization: `Bearer ${process.env.PAYWISE_PARTNER_KEY}` }, signal: AbortSignal.timeout(30000),
  });
  if (response.status !== 200) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
  const environment = response.headers.get("X-Paywise-Environment");
  if (environment !== "sandbox") throw new Error("Refusing Partner write outside the sandbox");
  ```

  ```java Java theme={null}
  import java.io.IOException;
  import java.net.URI;
  import java.net.http.HttpClient;
  import java.net.http.HttpRequest;
  import java.net.http.HttpResponse;
  import java.time.Duration;
  class ApiOnlyOnboardingRequest {
    public static void main(String[] args) throws IOException, InterruptedException {
      HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
      HttpRequest request = HttpRequest.newBuilder().uri(URI.create(System.getenv("PAYWISE_API_URL") + "/partner/v2/info/"))
          .timeout(Duration.ofSeconds(30)).header("Authorization", "Bearer " + System.getenv("PAYWISE_PARTNER_KEY")).GET().build();
      HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
      if (response.statusCode() != 200) throw new IOException(response.body());
      String environment = response.headers().firstValue("X-Paywise-Environment").orElse("");
      if (!"sandbox".equals(environment)) throw new IOException("Refusing Partner write outside the sandbox");
    }
  }
  ```

  ```csharp C# theme={null}
  using System;
  using System.Linq;
  using System.Net.Http;
  using System.Threading;
  using System.Threading.Tasks;
  class ApiOnlyOnboardingRequest {
      static async Task Main() {
          using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
          using var request = new HttpRequestMessage(HttpMethod.Get, $"{Environment.GetEnvironmentVariable("PAYWISE_API_URL")}/partner/v2/info/");
          request.Headers.Authorization = new("Bearer", Environment.GetEnvironmentVariable("PAYWISE_PARTNER_KEY"));
          using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(30));
          using var response = await client.SendAsync(request, cancellation.Token);
          if ((int)response.StatusCode != 200) throw new HttpRequestException(await response.Content.ReadAsStringAsync());
          if (!response.Headers.TryGetValues("X-Paywise-Environment", out var environmentValues)
              || environmentValues.Count() != 1
              || !string.Equals(environmentValues.Single(), "sandbox", StringComparison.Ordinal))
              throw new HttpRequestException("Refusing Partner write outside the sandbox");
      }
  }
  ```

  ```bash cURL theme={null}
  proof="$(curl --fail-with-body --silent --show-error --connect-timeout 5 --max-time 30 \
    --request GET "$PAYWISE_API_URL/partner/v2/info/" \
    --header "Authorization: Bearer $PAYWISE_PARTNER_KEY" \
    --dump-header - --output /dev/null --write-out '\n%{http_code}')"
  status="$(printf '%s\n' "$proof" | tail -n 1)"
  [ "$status" != "200" ] && exit 1
  environment="$(printf '%s\n' "$proof" | awk '
    {
      separator = index($0, ":")
      if (!separator || tolower(substr($0, 1, separator - 1)) != "x-paywise-environment") next
      count++
      value = substr($0, separator + 1)
      sub(/\r$/, "", value)
      sub(/^[ \t]*/, "", value)
      sub(/[ \t]*$/, "", value)
    }
    END { if (count != 1) exit 1; print value }
  ')" || exit 1
  [ "$environment" != "sandbox" ] && exit 1
  :
  ```
</CodeGroup>

## 4. Create the company aggregate

The inline admin is part of the same atomic command. Capture the returned
company and membership `id` fields directly; never search a list by name,
email, customer number, or first-row position.

<CodeGroup>
  ```python Python theme={null}
  import requests

  PAYWISE_API_URL = "https://api-sandbox.paywise.de"
  PAYWISE_PARTNER_KEY = "pw_sbx_your_partner_key"
  COMPANY_COMMAND_ID = "your-company-command-id"
  RUN_ID = "80000000-0000-4000-8000-000000000001"

  run_id = RUN_ID
  company_payload = {
      "name": f"Partner Quickstart {run_id}", "onboarding_mode": "api_only",
      "legal_form": "gmbh",
      "address": {"street": "Musterstraße 1", "postal_code": "10115", "city": "Berlin", "country": "DE"},
      "phone": "+49301234567", "tax_treatment": "input_tax_deductible",
      "vat_number": "DE129273398", "default_claim_type": "H05", "data_sharing_basis": "self_authorized",
      "payout_bank_account": {"account_holder": f"Partner Quickstart {run_id}", "iban": "DE89370400440532013000", "bic": None},
      "notification_channels": [{"type": "email", "value": f"operations-{run_id}@example.test", "notifications": ["status_updates", "requests_to_client", "statements"]}],
      "legal_representatives": [{"type": "managing_director", "name": "Alex Example"}],
      "users": [{"email": f"admin-{run_id}@example.test", "first_name": "Alex", "last_name": "Example", "role": "admin"}],
  }
  response = requests.post(
      f"{PAYWISE_API_URL}/partner/v2/companies/",
      headers={"Authorization": f"Bearer {PAYWISE_PARTNER_KEY}", "Content-Type": "application/json", "Idempotency-Key": COMPANY_COMMAND_ID},
      json=company_payload, timeout=(5, 30),
  )
  if response.status_code != 201:
      response.raise_for_status()
      raise RuntimeError(f"Expected 201, received {response.status_code}")
  ```

  ```javascript JavaScript theme={null}
  const runId = process.env.RUN_ID;
  const companyPayload = {
    name: `Partner Quickstart ${runId}`, onboarding_mode: "api_only", legal_form: "gmbh",
    address: { street: "Musterstraße 1", postal_code: "10115", city: "Berlin", country: "DE" },
    phone: "+49301234567", tax_treatment: "input_tax_deductible", vat_number: "DE129273398", default_claim_type: "H05", data_sharing_basis: "self_authorized",
    payout_bank_account: { account_holder: `Partner Quickstart ${runId}`, iban: "DE89370400440532013000", bic: null },
    notification_channels: [{ type: "email", value: `operations-${runId}@example.test`, notifications: ["status_updates", "requests_to_client", "statements"] }],
    legal_representatives: [{ type: "managing_director", name: "Alex Example" }],
    users: [{ email: `admin-${runId}@example.test`, first_name: "Alex", last_name: "Example", role: "admin" }],
  };
  const response = await fetch(`${process.env.PAYWISE_API_URL.replace(/\/$/, "")}/partner/v2/companies/`, {
    method: "POST", headers: { Authorization: `Bearer ${process.env.PAYWISE_PARTNER_KEY}`, "Content-Type": "application/json", "Idempotency-Key": process.env.COMPANY_COMMAND_ID },
    body: JSON.stringify(companyPayload), signal: AbortSignal.timeout(30000),
  });
  if (response.status !== 201) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
  ```

  ```java Java theme={null}
  import java.io.IOException;
  import java.net.URI;
  import java.net.http.HttpClient;
  import java.net.http.HttpRequest;
  import java.net.http.HttpResponse;
  import java.time.Duration;

  class ApiOnlyOnboardingRequest {
    static String jsonString(String value) {
      StringBuilder escaped = new StringBuilder("\"");
      for (int index = 0; index < value.length(); index++) {
        char character = value.charAt(index);
        switch (character) {
          case '\"': escaped.append("\\\""); break;
          case '\\': escaped.append("\\\\"); break;
          case '\n': escaped.append("\\n"); break;
          case '\r': escaped.append("\\r"); break;
          case '\t': escaped.append("\\t"); break;
          default: if (character < 0x20) escaped.append(String.format("\\u%04x", (int) character)); else escaped.append(character);
        }
      }
      return escaped.append('\"').toString();
    }
    public static void main(String[] args) throws IOException, InterruptedException {
      String run = System.getenv("RUN_ID");
      String json = "{\"name\":" + jsonString("Partner Quickstart " + run) + ",\"onboarding_mode\":\"api_only\",\"legal_form\":\"gmbh\",\"address\":{\"street\":\"Musterstraße 1\",\"postal_code\":\"10115\",\"city\":\"Berlin\",\"country\":\"DE\"},\"phone\":\"+49301234567\",\"tax_treatment\":\"input_tax_deductible\",\"vat_number\":\"DE129273398\",\"default_claim_type\":\"H05\",\"data_sharing_basis\":\"self_authorized\",\"payout_bank_account\":{\"account_holder\":" + jsonString("Partner Quickstart " + run) + ",\"iban\":\"DE89370400440532013000\",\"bic\":null},\"notification_channels\":[{\"type\":\"email\",\"value\":" + jsonString("operations-" + run + "@example.test") + ",\"notifications\":[\"status_updates\",\"requests_to_client\",\"statements\"]}],\"legal_representatives\":[{\"type\":\"managing_director\",\"name\":\"Alex Example\"}],\"users\":[{\"email\":" + jsonString("admin-" + run + "@example.test") + ",\"first_name\":\"Alex\",\"last_name\":\"Example\",\"role\":\"admin\"}]}";
      HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
      HttpRequest request = HttpRequest.newBuilder().uri(URI.create(System.getenv("PAYWISE_API_URL") + "/partner/v2/companies/"))
          .timeout(Duration.ofSeconds(30)).header("Authorization", "Bearer " + System.getenv("PAYWISE_PARTNER_KEY"))
          .header("Content-Type", "application/json").header("Idempotency-Key", System.getenv("COMPANY_COMMAND_ID"))
          .POST(HttpRequest.BodyPublishers.ofString(json)).build();
      HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
      if (response.statusCode() != 201) throw new IOException(response.body());
    }
  }
  ```

  ```csharp C# theme={null}
  using System;
  using System.Net.Http;
  using System.Net.Http.Json;
  using System.Threading;
  using System.Threading.Tasks;
  class ApiOnlyOnboardingRequest {
      static async Task Main() {
          var run = Environment.GetEnvironmentVariable("RUN_ID");
          var payload = new {
              name = $"Partner Quickstart {run}", onboarding_mode = "api_only", legal_form = "gmbh", address = new { street = "Musterstraße 1", postal_code = "10115", city = "Berlin", country = "DE" },
              phone = "+49301234567", tax_treatment = "input_tax_deductible", vat_number = "DE129273398", default_claim_type = "H05", data_sharing_basis = "self_authorized",
              payout_bank_account = new { account_holder = $"Partner Quickstart {run}", iban = "DE89370400440532013000", bic = (string?)null },
              notification_channels = new[] { new { type = "email", value = $"operations-{run}@example.test", notifications = new[] { "status_updates", "requests_to_client", "statements" } } },
              legal_representatives = new[] { new { type = "managing_director", name = "Alex Example" } },
              users = new[] { new { email = $"admin-{run}@example.test", first_name = "Alex", last_name = "Example", role = "admin" } }
          };
          using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
          using var request = new HttpRequestMessage(HttpMethod.Post, $"{Environment.GetEnvironmentVariable("PAYWISE_API_URL")}/partner/v2/companies/");
          request.Headers.Authorization = new("Bearer", Environment.GetEnvironmentVariable("PAYWISE_PARTNER_KEY"));
          request.Headers.Add("Idempotency-Key", Environment.GetEnvironmentVariable("COMPANY_COMMAND_ID"));
          request.Content = JsonContent.Create(payload);
          using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(30));
          using var response = await client.SendAsync(request, cancellation.Token);
          if ((int)response.StatusCode != 201) throw new HttpRequestException(await response.Content.ReadAsStringAsync());
      }
  }
  ```

  ```bash cURL theme={null}
  status="$(curl --fail-with-body --silent --show-error --connect-timeout 5 --max-time 30 \
    --request POST "$PAYWISE_API_URL/partner/v2/companies/" \
    --header "Authorization: Bearer $PAYWISE_PARTNER_KEY" \
    --header "Content-Type: application/json" \
    --header "Idempotency-Key: $COMPANY_COMMAND_ID" \
    --output company-created.json --write-out '%{http_code}' \
    --data @- <<JSON
  {"name":"Partner Quickstart $RUN_ID","onboarding_mode":"api_only","legal_form":"gmbh","address":{"street":"Musterstraße 1","postal_code":"10115","city":"Berlin","country":"DE"},"phone":"+49301234567","tax_treatment":"input_tax_deductible","vat_number":"DE129273398","default_claim_type":"H05","data_sharing_basis":"self_authorized","payout_bank_account":{"account_holder":"Partner Quickstart $RUN_ID","iban":"DE89370400440532013000","bic":null},"notification_channels":[{"type":"email","value":"operations-$RUN_ID@example.test","notifications":["status_updates","requests_to_client","statements"]}],"legal_representatives":[{"type":"managing_director","name":"Alex Example"}],"users":[{"email":"admin-$RUN_ID@example.test","first_name":"Alex","last_name":"Example","role":"admin"}]}
  JSON
  )"
  [ "$status" != "201" ] && exit 1
  ```
</CodeGroup>

## 5. Reconcile the exact company URL

<CodeGroup>
  ```python Python theme={null}
  import requests

  PAYWISE_API_URL = "https://api-sandbox.paywise.de"
  PAYWISE_PARTNER_KEY = "pw_sbx_your_partner_key"
  PAYWISE_COMPANY_ID = "20000000-0000-4000-8000-000000000001"

  response = requests.get(
      f"{PAYWISE_API_URL}/partner/v2/companies/{PAYWISE_COMPANY_ID}/",
      headers={"Authorization": f"Bearer {PAYWISE_PARTNER_KEY}"},
      timeout=(5, 30),
  )
  if response.status_code != 200:
      response.raise_for_status()
      raise RuntimeError(f"Expected 200, received {response.status_code}")
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch(`${process.env.PAYWISE_API_URL.replace(/\/$/, "")}/partner/v2/companies/${process.env.PAYWISE_COMPANY_ID}/`, {
    method: "GET", headers: { Authorization: `Bearer ${process.env.PAYWISE_PARTNER_KEY}` }, signal: AbortSignal.timeout(30000),
  });
  if (response.status !== 200) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
  ```

  ```java Java theme={null}
  import java.io.IOException;
  import java.net.URI;
  import java.net.http.HttpClient;
  import java.net.http.HttpRequest;
  import java.net.http.HttpResponse;
  import java.time.Duration;
  class ApiOnlyOnboardingRequest {
    public static void main(String[] args) throws IOException, InterruptedException {
      String url = System.getenv("PAYWISE_API_URL") + "/partner/v2/companies/" + System.getenv("PAYWISE_COMPANY_ID") + "/";
      HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
      HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).timeout(Duration.ofSeconds(30))
          .header("Authorization", "Bearer " + System.getenv("PAYWISE_PARTNER_KEY")).GET().build();
      HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
      if (response.statusCode() != 200) throw new IOException(response.body());
    }
  }
  ```

  ```csharp C# theme={null}
  using System;
  using System.Net.Http;
  using System.Threading;
  using System.Threading.Tasks;
  class ApiOnlyOnboardingRequest {
      static async Task Main() {
          using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
          using var request = new HttpRequestMessage(HttpMethod.Get, $"{Environment.GetEnvironmentVariable("PAYWISE_API_URL")}/partner/v2/companies/{Environment.GetEnvironmentVariable("PAYWISE_COMPANY_ID")}/");
          request.Headers.Authorization = new("Bearer", Environment.GetEnvironmentVariable("PAYWISE_PARTNER_KEY"));
          using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(30));
          using var response = await client.SendAsync(request, cancellation.Token);
          if ((int)response.StatusCode != 200) throw new HttpRequestException(await response.Content.ReadAsStringAsync());
      }
  }
  ```

  ```bash cURL theme={null}
  status="$(curl --fail-with-body --silent --show-error --connect-timeout 5 --max-time 30 \
    --request GET "$PAYWISE_API_URL/partner/v2/companies/$PAYWISE_COMPANY_ID/" \
    --header "Authorization: Bearer $PAYWISE_PARTNER_KEY" \
    --output company-current.json --write-out '%{http_code}')"
  [ "$status" != "200" ] && exit 1
  ```
</CodeGroup>

## Runnable onboarding workflow

This complete workflow bounds ambiguous-response recovery, proves the sandbox
before every write attempt, captures all returned UUIDs, and writes the
one-time webhook secret without logging it.

```python Python workflow theme={null}
import os
import time
import uuid
import requests

PAYWISE_API_URL = "https://api-sandbox.paywise.de"
PAYWISE_PARTNER_KEY = "pw_sbx_your_partner_key"
COMPANY_COMMAND_ID = "your-company-command-id"
PAYWISE_WEBHOOK_SECRET_FILE = "./paywise-webhook-secret.txt"
RUN_ID = "80000000-0000-4000-8000-000000000001"
WEBHOOK_COMMAND_ID = "your-webhook-command-id"

base_url = PAYWISE_API_URL
if not base_url.startswith("https://"):
    raise ValueError("PAYWISE_API_URL must be HTTPS")
session = requests.Session()
session.headers.update({"Authorization": f"Bearer {PAYWISE_PARTNER_KEY}"})

def expect(response, expected_status):
    if response.status_code != expected_status:
        response.raise_for_status()
        raise RuntimeError(f"Expected {expected_status}, received {response.status_code}")
    return response

def prove_sandbox():
    response = session.get(f"{base_url}/partner/v2/info/", timeout=(5, 30))
    if response.status_code != 200:
        response.raise_for_status()
        raise RuntimeError(f"Expected 200, received {response.status_code}")
    environment = next((value for name, value in response.headers.items() if name.lower() == "x-paywise-environment"), None)
    if environment != "sandbox":
        raise RuntimeError("Refusing Partner write outside the sandbox")

def command(url, expected_status, key, payload=None):
    headers = {"Idempotency-Key": key}
    if payload is not None:
        headers["Content-Type"] = "application/json"
    for attempt in range(3):
        prove_sandbox()
        try:
            response = session.post(url, headers=headers, json=payload, timeout=(5, 30))
        except requests.Timeout:
            if attempt == 2:
                raise
            time.sleep(attempt + 1)
            continue
        if response.status_code != expected_status:
            response.raise_for_status()
            raise RuntimeError(f"Expected {expected_status}, received {response.status_code}")
        return response.json()
    raise RuntimeError("Command retry budget exhausted")

webhook_payload = {
    "url": "https://hooks.example.test/paywise-partner",
    "enabled": True,
    "events": ["company.created", "company.access.confirmed", "company.case_submission_readiness.changed"],
    "description": "Partner onboarding lifecycle",
}
created_webhook = command(f"{base_url}/partner/v2/webhooks/", 201, WEBHOOK_COMMAND_ID, webhook_payload)
webhook_id = str(uuid.UUID(created_webhook["id"]))
secret = created_webhook.pop("secret_key")
secret_path = PAYWISE_WEBHOOK_SECRET_FILE
descriptor = os.open(secret_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
with os.fdopen(descriptor, "w", encoding="utf-8") as secret_file:
    secret_file.write(secret)
os.chmod(secret_path, 0o600)
del secret

run_id = RUN_ID
company_payload = {
    "name": f"Partner Quickstart {run_id}", "onboarding_mode": "api_only",
    "legal_form": "gmbh",
    "address": {"street": "Musterstraße 1", "postal_code": "10115", "city": "Berlin", "country": "DE"},
    "phone": "+49301234567", "tax_treatment": "input_tax_deductible", "vat_number": "DE129273398",
    "default_claim_type": "H05", "data_sharing_basis": "self_authorized",
    "payout_bank_account": {"account_holder": f"Partner Quickstart {run_id}", "iban": "DE89370400440532013000", "bic": None},
    "notification_channels": [{"type": "email", "value": f"operations-{run_id}@example.test", "notifications": ["status_updates", "requests_to_client", "statements"]}],
    "legal_representatives": [{"type": "managing_director", "name": "Alex Example"}],
    "users": [{"email": f"admin-{run_id}@example.test", "first_name": "Alex", "last_name": "Example", "role": "admin"}],
}
created_company = command(f"{base_url}/partner/v2/companies/", 201, COMPANY_COMMAND_ID, company_payload)
company_id = str(uuid.UUID(created_company["id"]))
membership_ids = [str(uuid.UUID(item["id"])) for item in created_company.get("users", [])]
if not membership_ids:
    raise RuntimeError("Company response did not return an inline membership UUID")
current = session.get(f"{base_url}/partner/v2/companies/{company_id}/", timeout=(5, 30))
if current.status_code != 200:
    current.raise_for_status()
    raise RuntimeError(f"Expected 200, received {current.status_code}")
if current.json().get("id") != company_id:
    raise RuntimeError("Company detail identity mismatch")
WORKFLOW_RESULT = {"webhook_id": webhook_id, "company_id": company_id, "membership_ids": membership_ids}
```

## Representative response

```http theme={null}
HTTP/1.1 201 Created
Content-Type: application/json
X-Paywise-Environment: sandbox

{
  "id": "10000000-0000-4000-8000-000000000001",
  "customer_number": "5G0123",
  "name": "Example Client GmbH",
  "onboarding_mode": "api_only",
  "onboarding_status": "confirmation_not_required",
  "legal_form": "gmbh",
  "address": {
    "street": "Musterstraße 1",
    "postal_code": "10115",
    "city": "Berlin",
    "country": "DE"
  },
  "phone": "+49301234567",
  "tax_treatment": "input_tax_deductible",
  "vat_number": "DE129273398",
  "default_claim_type": "H05",
  "payout_bank_account": {
    "account_holder": "Example Client GmbH",
    "iban": "DE89370400440532013000",
    "bic": null
  },
  "notification_channels": [{
    "type": "email",
    "value": "operations@example.test",
    "notifications": ["status_updates", "requests_to_client", "statements"]
  }],
  "legal_representatives": [{
    "type": "managing_director",
    "name": "Alex Example"
  }],
  "case_access": "available",
  "case_submission_readiness": {"ready": true, "issues": []},
  "users": [{
    "id": "11000000-0000-4000-8000-000000000001",
    "email": "admin@example.test",
    "first_name": "Alex",
    "last_name": "Example",
    "role": "admin",
    "status": "pending_setup",
    "invite_expires_at": "2026-09-06T10:00:00Z",
    "setup_url": "https://app.paywise.de/account/einladung/14000000-0000-4000-8000-000000000001/",
    "revoked_at": null,
    "revoked_by_token_id": null,
    "revocation_reason": "",
    "sandbox_origin": "sandbox_native",
    "created_at": "2026-08-27T10:00:00Z",
    "updated_at": "2026-08-27T10:00:00Z"
  }],
  "sandbox_origin": "sandbox_native",
  "created_at": "2026-08-27T10:00:00Z",
  "updated_at": "2026-08-27T10:00:00Z"
}
```

## Failure and recovery

* Replay an ambiguous webhook or company create only with its original key and
  byte-equivalent body. Bound the attempts; never generate a duplicate command.
* A validation error starts a new logical command only after correcting the
  rejected fields. `409 idempotency_key_expired` is terminal for that key.
* Render every readiness issue. Keep draft creation available, but require
  current readiness and current access immediately before finalization.
* The fresh company-create response returns each newly issued `setup_url`
  once; reads and an idempotent replay omit it. Setup mail is also delivered
  to the invitee. Do not infer whether an inline email created or reused a
  global user.

## Related reference

* [GET `/partner/v2/info/`](/api-docs/partner-api/reference/meta/get-partner-token-info)
* [POST `/partner/v2/webhooks/`](/api-docs/partner-api/reference/webhooks/create-webhook)
* [POST `/partner/v2/companies/`](/api-docs/partner-api/reference/companies/onboard-a-company)
* [GET `/partner/v2/companies/{id}/`](/api-docs/partner-api/reference/companies/get-a-managed-company)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.