PHP example

One whole script, top to bottom. It creates a customer and prints the id NolaPro gave it.

First, a token#

The script needs an API token. You create one in NolaPro itself, not here. Tokens live on the user they belong to rather than on a page of their own: My Info for your own, Admin → user edit for someone else's. You choose the scopes the token may use as you create it, and creating a customer needs customers:write.

Authentication walks through it, including what the scopes mean and what to do when a token is refused. The token is shown once, when you create it. There is no screen that will show it to you again, so put it somewhere your code can read before you close the dialog.

Never paste the token into the script itself. The example reads it from the environment, which keeps it out of your repository and out of your error logs:

export NP_TOKEN='np_v2_...'

Creating a customer#

<?php

$base  = 'https://acme.nolapro.com/!/api/v2';   // your NolaPro address plus /!/api/v2
$token = getenv('NP_TOKEN');

$customer = array(
    'companyname'  => 'Bridgewater Fabrication',
    'customercode' => 'BRIDGE-01',
    'email1'       => 'ap@bridgewater.example',
    'invoiceterms' => 'Net 30',
);

$ch = curl_init($base . '/customers');
curl_setopt_array($ch, array(
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POSTFIELDS     => json_encode($customer),
    CURLOPT_HTTPHEADER     => array(
        'Authorization: Bearer ' . $token,
        'Content-Type: application/json',
        // Same key on a retry returns the ORIGINAL record instead of making
        // a second customer. Use something you can regenerate from your own
        // data, not a random value.
        'Idempotency-Key: customer-BRIDGE-01',
    ),
));

$body   = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);

if ($body === false) {
    exit('Could not reach NolaPro: ' . curl_error($ch) . "\n");
}

$result = json_decode($body, true);

if ($status === 201) {
    echo "Created customer {$result['id']} ({$result['companyname']})\n";
} else {
    // Every refusal has this shape: a stable `code` to branch on, a `message`
    // for a human, and `field` when one field is to blame.
    $e = $result['error'];
    echo "HTTP {$status} {$e['code']}: {$e['message']}\n";
    if (isset($e['field'])) {
        echo "  the field at fault: {$e['field']}\n";
    }
}

Run it and you get:

Created customer 1042 (Bridgewater Fabrication)

What to copy carefully#

email1, not email. Customers carry two address fields.

invoiceterms is the term's own wording"Net 30", not "NET30". It resolves against the terms set up in your install; see Pointing at another record.

Branch on code, never on message. The codes are stable and listed in Errors; the wording is not, and it is translated.

Send the Idempotency-Key. If the response never arrives — a timeout, a dropped connection — you cannot tell whether the customer was created. Retry with the same key and you get the original record back rather than a duplicate. Build the key from your own data, so a retry naturally reproduces it.

The same shape everywhere#

Nothing above is specific to customers. Change the URL and the body and the same script creates an item, a vendor, or a project. A parent and its children go in one request — an invoice carries its lines, a project carries its milestones — and the whole thing is written in one transaction, so a rejected line means no invoice rather than half of one.

Recipes has the multi-step tasks: quote to order to invoice, receiving stock against a purchase order, and applying a payment.