Integrating Third-Party APIs In Drupal With Custom Services
Connecting a Drupal website to an external application is rarely just a matter of sending an HTTP request. A production integration needs clear service boundaries, authentication handling, useful error messages, sensible caching, and a way to test behaviour without repeatedly contacting the remote system. Building these concerns into a custom Drupal service keeps controllers, plugins, forms, and preprocess functions focused on their actual jobs.
This approach works well for payment gateways, CRM platforms, address validation tools, weather feeds, inventory systems, marketing platforms, and Australian services such as postcode lookup or business directory APIs. Drupal’s dependency injection container and Guzzle-based HTTP client provide the foundation, while a small custom module can turn an unreliable external dependency into a predictable part of the site.
Plan The Integration Around A Service
A custom service should represent a useful business capability rather than expose low-level HTTP operations everywhere in the codebase. For example, WeatherClient could provide current conditions for a location, while CustomerApi could create or update a contact in a CRM. A controller should call a method such as findCustomerByEmail(), not construct URLs, headers, and JSON payloads itself.
This separation makes the integration easier to replace and maintain. If the provider changes its endpoint structure, only the service and its tests should need significant updates. It also prevents duplicated authentication and error handling across a form submission, a queue worker, and a scheduled task.
Before writing code, document the provider’s base URL, endpoints, HTTP methods, required headers, authentication scheme, response format, rate limits, timeout rules, and sandbox details. Check whether the provider treats Australian state abbreviations, postcodes, phone numbers, and time zones differently from its default market. A logistics API, for example, may require Australia/Sydney rather than a generic server time zone when calculating delivery dates.
Register A Drupal Service And HTTP Client
Create a custom module with a service definition in example_api.services.yml. Drupal already provides an HTTP client factory, so the service can receive a Guzzle client through dependency injection rather than creating a client directly inside a method.
services:
example_api.client:
class: Drupal\example_api\Service\ExampleApiClient
arguments:
- '@http_client_factory'
- '@config.factory'
- '@logger.factory'
The class can request a client configured with a base URI and a default timeout. Injecting config.factory allows credentials and endpoint settings to be managed through Drupal configuration, while logger.factory provides a channel dedicated to the integration.
namespace Drupal\example_api\Service;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Http\ClientFactory;
use Psr\Log\LoggerInterface;
final class ExampleApiClient {
private $client;
private LoggerInterface $logger;
public function __construct(
ClientFactory $http_client_factory,
ConfigFactoryInterface $config_factory,
LoggerInterface $logger_factory,
) {
$settings = $config_factory->get('example_api.settings');
$this->client = $http_client_factory->fromOptions([
'base_uri' => rtrim($settings->get('base_url'), '/') . '/',
'timeout' => 10,
'connect_timeout' => 5,
'http_errors' => FALSE,
]);
$this->logger = $logger_factory->get('example_api');
}
}
The exact constructor signature can vary with the Drupal version and injected service. The important principle is consistent: use Drupal’s container, keep configuration outside the class, and set conservative network timeouts. A remote API should never hold a page request open indefinitely, particularly for visitors using mobile connections in regional New South Wales or Western Australia.
Keep Credentials And Configuration Secure
API keys, client secrets, and bearer tokens should not be committed to a module’s .yml files or stored as ordinary exported configuration. Drupal configuration is commonly moved between development, staging, and production environments, so a secret included in a configuration export can travel much further than intended.
A common arrangement stores non-sensitive values such as an API base URL, account identifier, and enabled flag in Drupal configuration. Secrets are supplied through environment variables, settings.php, a secrets manager, or a contributed configuration solution appropriate to the hosting environment. The service can read those values at runtime without displaying them in the administrative interface.
If the provider uses OAuth 2.0, keep token acquisition in a dedicated component or method. Cache access tokens until shortly before their expiry and refresh them when required. Do not log the Authorization header, complete request URLs containing credentials, or response bodies that might include customer data. Australian privacy obligations and the Privacy Act 1988 make careful handling of personal information particularly important for CRM, health, education, and government-related integrations.
Configuration forms should validate URLs, required identifiers, and allowed environments. A production website should not silently point at a sandbox account. Display a clear status message when credentials are missing, but show detailed connection diagnostics only to authorised administrators.
Build Predictable Requests And Responses
A service method should validate inputs before making a request and should normalise the provider’s response into a structure that Drupal code can use. This prevents every caller from learning the external API’s naming conventions and nested response format.
public function findCustomerByEmail(string $email): ?array {
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException('A valid email address is required.');
}
try {
$response = $this->client->request('GET', 'customers', [
'query' => ['email' => $email],
'headers' => [
'Accept' => 'application/json',
],
]);
$status = $response->getStatusCode();
if ($status === 404) {
return NULL;
}
if ($status < 200 || $status >= 300) {
throw new \RuntimeException('The customer API returned HTTP ' . $status);
}
$data = json_decode((string) $response->getBody(), TRUE, 512, JSON_THROW_ON_ERROR);
return $this->normaliseCustomer($data);
}
catch (\Throwable $exception) {
$this->logger->error('Customer lookup failed: @message', [
'@message' => $exception->getMessage(),
]);
throw new \RuntimeException('The customer service is currently unavailable.', 0, $exception);
}
}
Use POST, PUT, or PATCH according to the provider’s documentation, and send structured data using Guzzle’s json option where appropriate. Treat a 404 differently from a 500: the first may mean that no record exists, while the second indicates a service failure. Callers need that distinction so a missing customer does not appear as a site outage.
For an integration used in a web form, catch the service exception at the form or controller boundary and provide a helpful message. For a queue worker, allow retryable failures to be retried and record permanent validation failures separately. Never expose raw provider messages, stack traces, or response payloads to site visitors.
Handle Authentication, Rate Limits, And Retries
Authentication varies from a static API key to signed requests, OAuth bearer tokens, or mutual TLS. Put provider-specific header construction in one place. A static key might be sent as X-Api-Key, while another service expects Authorization: Bearer ...; the rest of the Drupal code should not care which method is used.
Retries require restraint. Network timeouts, connection resets, and HTTP 429 or selected 5xx responses may be temporary. Retrying a POST that creates an order can create duplicates unless the provider supports idempotency keys. Prefer a bounded retry strategy with exponential backoff, and use Drupal’s Queue API for work that does not need to finish during the visitor’s request.
Rate limits also affect architecture. If an Australian real estate website requests suburb information on every page view, it can quickly waste its provider quota. Cache stable results, batch requests where the API supports batching, and move bulk synchronisation into cron or queue processing. Record rate-limit headers when available so administrators can see whether a problem is caused by local code or an exhausted vendor allowance.
Cache External Data Without Hiding Failures
Drupal’s cache system is useful for API responses that remain valid for a defined period. A weather reading may be cached for several minutes, while an Australian postcode-to-suburb mapping could remain cached for much longer. Include all meaningful request parameters in the cache key, including language, location, account, and any filters.
Cache metadata should reflect the source and expiry of the data. If the service is used in render arrays, attach cache tags and max-age values where they make sense. For a standalone service, Drupal’s cache backend can store a normalised response and an explicit expiration time.
Avoid treating stale data as current without a policy. A short stale-if-error window can keep a public page useful during a provider outage, but the interface should make it clear when information may be delayed. Do not cache private customer records in a shared cache unless the cache context and access controls have been designed carefully.
For content shown to visitors in Melbourne, Brisbane, or Perth, local time presentation should happen after retrieval and according to the site’s requirements. Store API timestamps in a stable format, normally UTC, then convert them for display. This avoids daylight-saving errors between places such as Sydney and Queensland, which does not observe daylight saving.
Test The Service Without Calling The Provider
Unit tests should verify request construction, response normalisation, validation, and exception handling without making real network calls. Guzzle’s mock handler or a mocked HTTP client can return representative responses for success, not-found, rate-limit, malformed JSON, and server-error cases. Include fixtures based on the provider’s actual payloads, with personal data removed.
Kernel tests are useful when the service depends on Drupal configuration, logging, cache backends, or the service container. A test can install the module, set temporary API configuration, invoke the service, and confirm that the expected cache entry or log event is produced. Functional tests can then cover the administrative configuration form and user-facing behaviour.
Test Australian-specific values rather than assuming an American address model. Include four-digit postcodes such as 3000, state codes such as NSW and QLD, mobile numbers in the format commonly used locally, and dates around daylight-saving transitions. Also test Unicode names and addresses, because customer data may include accented characters and names from many language backgrounds.
A staging environment should use sandbox credentials and synthetic records. Add monitoring for response time, error rate, authentication failures, and queue backlog. A small status report on the Drupal administration page can help a site editor distinguish a misconfigured API key from a provider outage without granting access to sensitive logs.
Operational Recommendations For A Reliable Integration
A maintainable integration benefits from a few practical rules applied consistently across the custom module:
- Give the service a clear domain name and keep HTTP details inside it.
- Store credentials outside exported configuration and redact secrets from logs.
- Set connection and total request timeouts rather than relying on defaults.
- Treat 404, 422, 429, and 5xx responses as different operational conditions.
- Use cache bins, queue workers, and cron for repeated or non-interactive API work.
- Add unit tests with mocked responses before connecting a production account.
- Document scopes, rate limits, webhook requirements, and the process for rotating credentials.
Webhooks deserve special attention when the provider can notify Drupal about changes. Verify signatures before processing payloads, reject old timestamps where the scheme supports replay protection, and return a fast response before performing lengthy work. Store the event identifier so a retried webhook does not process the same order or contact twice.
Finally, document ownership and failure procedures. Record who receives alerts, how to rotate credentials, which service account is used, and what the site should display during an outage. An integration that works in a developer’s local environment but has no operational path for a Saturday incident is unfinished. With a well-defined Drupal service, secure configuration, controlled network behaviour, and realistic tests, external APIs become a manageable part of the application rather than a hidden source of fragile page requests.